diff --git a/.gitignore b/.gitignore index 71c9194e8bd..2c744a4e65a 100644 --- a/.gitignore +++ b/.gitignore @@ -8,11 +8,10 @@ src/main/resources/docs/ /out/ /*.iml -# Storage/log files -/data/ -/config.json -/preferences.json -/*.log.* +## Storage/log files +#/config.json +#/preferences.json +#/*.log.* # Test sandbox files src/test/data/sandbox/ @@ -20,3 +19,7 @@ src/test/data/sandbox/ # MacOS custom attributes files created by Finder .DS_Store docs/_site/ + +# Error logs +*.log +/bin diff --git a/README.md b/README.md index 13f5c77403f..cd8ad3068fa 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,23 @@ -[![CI Status](https://github.com/se-edu/addressbook-level3/workflows/Java%20CI/badge.svg)](https://github.com/se-edu/addressbook-level3/actions) - -![Ui](docs/images/Ui.png) - -* This is **a sample project for Software Engineering (SE) students**.
- Example usages: - * as a starting point of a course project (as opposed to writing everything from scratch) - * as a case study -* The project simulates an ongoing software project for a desktop application (called _AddressBook_) used for managing contact details. - * It is **written in OOP fashion**. It provides a **reasonably well-written** code base **bigger** (around 6 KLoC) than what students usually write in beginner-level SE modules, without being overwhelmingly big. - * It comes with a **reasonable level of user and developer documentation**. -* It is named `AddressBook Level 3` (`AB3` for short) because it was initially created as a part of a series of `AddressBook` projects (`Level 1`, `Level 2`, `Level 3` ...). -* For the detailed documentation of this project, see the **[Address Book Product Website](https://se-education.org/addressbook-level3)**. -* This project is a **part of the se-education.org** initiative. If you would like to contribute code to this project, see [se-education.org](https://se-education.org#https://se-education.org/#contributing) for more info. +[![CI Status](https://github.com/AY2223S2-CS2103T-W09-2/tp/workflows/Java%20CI/badge.svg)](https://github.com/AY2223S2-CS2103T-W09-2/tp/actions) +[![codecov](https://codecov.io/gh/AY2223S2-CS2103T-W09-2/tp/branch/master/graph/badge.svg)](https://codecov.io/gh/AY2223S2-CS2103T-W09-2/tp/) + + +![UiMockup](docs/images/Ui.png) + +# FastTrack +FastTrack is specifically designed to cater to the needs of tech-savvy NUS computing students who are on a tight budget. +With our expense tracking features, students can easily track their daily expenses and ensure that they are not overspending. +The Command Line Interface (CLI) and batch logging features make it easy to enter expenses quickly and efficiently. +which is ideal stressed out undergraduates with limited free times. + +### Core Features +* Log and delete expenses +* Create and delete expense categories +* Filter expenses by categories +* Graphical User Interface (GUI) displaying spending statistics to allows students to view their financial situation at a single glance and understand how to improve their spending habits to keep within predefined budgets + +### Aim +By providing an easy-to-use, feature-rich expense tracker which focuses on speed, the user is able to log expenses quickly and view their spending habits, giving them the best of both worlds. + + +This project is based on the AddressBook-Level3 project created by the [SE-EDU Initiative](http://se-education.org). diff --git a/build.gradle b/build.gradle index 108397716bd..5523188c902 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ plugins { id 'jacoco' } -mainClassName = 'seedu.address.Main' +mainClassName = 'fasttrack.Main' sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 @@ -65,8 +65,12 @@ dependencies { testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: jUnitVersion } +run { + enableAssertions = true +} + shadowJar { - archiveFileName = 'addressbook.jar' + archiveFileName = 'fasttrack.jar' } defaultTasks 'clean', 'test' diff --git a/config.json b/config.json new file mode 100644 index 00000000000..a0edc78a820 --- /dev/null +++ b/config.json @@ -0,0 +1,4 @@ +{ + "logLevel" : "INFO", + "userPrefsFilePath" : "preferences.json" +} diff --git a/data/fastTrack.json b/data/fastTrack.json new file mode 100644 index 00000000000..1149c4bdc12 --- /dev/null +++ b/data/fastTrack.json @@ -0,0 +1,63 @@ +{ + "categories" : [ { + "categoryName" : "Food", + "summary" : "For food" + }, { + "categoryName" : "Entertainment", + "summary" : "For entertainment" + }, { + "categoryName" : "Transportation", + "summary" : "For bus, car, train" + }, { + "categoryName" : "Shopping", + "summary" : "" + }, { + "categoryName" : "Housing", + "summary" : "" + } ], + "expenses" : [ { + "name" : "Meal at JE", + "amount" : "4.5", + "date" : "2023-04-10", + "category" : { + "categoryName" : "Food", + "summary" : "For food" + } + }, { + "name" : "Groceries", + "amount" : "56.3", + "date" : "2023-03-25", + "category" : { + "categoryName" : "Food", + "summary" : "For food" + } + }, { + "name" : "Shoes", + "amount" : "75.0", + "date" : "2023-03-20", + "category" : { + "categoryName" : "Shopping", + "summary" : "" + } + }, { + "name" : "Movie ticket", + "amount" : "12.99", + "date" : "2023-03-15", + "category" : { + "categoryName" : "Entertainment", + "summary" : "For entertainment" + } + }, { + "name" : "MRT fare", + "amount" : "45.8", + "date" : "2023-03-10", + "category" : { + "categoryName" : "Transportation", + "summary" : "For bus, car, train" + } + } ], + "budget" : { + "amount" : "0.0" + }, + "recurringGenerators" : [ ] +} diff --git a/docs/AboutUs.md b/docs/AboutUs.md index 1c9514e966a..ca844915d2f 100644 --- a/docs/AboutUs.md +++ b/docs/AboutUs.md @@ -9,51 +9,55 @@ You can reach us at the email `seer[at]comp.nus.edu.sg` ## Project team -### John Doe +### Shirshajit Sen Gupta - + -[[homepage](http://www.comp.nus.edu.sg/~damithch)] -[[github](https://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[homepage](https://shirsho-12.github.io)] +[[github](https://github.com/shirsho-12)] +[[portfolio](team/gitsac.md)] -* Role: Project Advisor +* Role: Manage the repository, implement features, perform testing +* Responsibilities: Project Management, Feature Implementation, Test Management -### Jane Doe +### Shi Wen Hong - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](http://github.com/jinbesan)] +[[portfolio](team/jinbesan.md)] -* Role: Team Lead -* Responsibilities: UI +* Role: Developer, Technical Writer +* Responsibilities: Feature Implementation and maintenance of documentation. -### Johnny Doe +### Randall Ng Hong Rong - + -[[github](http://github.com/johndoe)] [[portfolio](team/johndoe.md)] +[[github](http://github.com/randallnhr)] +[[portfolio](team/randallnhr.md)] * Role: Developer -* Responsibilities: Data +* Responsibilities: Logic + +### Nicholas Lee Jun Yi -### Jean Doe + - +[[github](http://github.com/nicleejy)] -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[portfolio](team/nicleejy.md)] * Role: Developer -* Responsibilities: Dev Ops + Threading +* Responsibilities: UI -### James Doe +### Isaac Chai Han Jie - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](http://github.com/gitsac)] +[[portfolio](team/gitsac.md)] + +* Role: Developer and Task-keeper` +* Responsibilities: Logic and keep the team repository clean. -* Role: Developer -* Responsibilities: UI diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 46eae8ee565..80b9c97331a 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -2,64 +2,149 @@ layout: page title: Developer Guide --- -* Table of Contents -{:toc} --------------------------------------------------------------------------------------------------------------------- +# **Table of Contents** + {:toc} +1. [Introduction to FastTrack](#fasttrack) +2. [Purpose of this guide](#purpose-of-this-guide) +3. [How to use this guide](#how-to-use-this-guide) +4. [Acknowledgements](#acknowledgements) +5. [Setting up and getting started](#setting-up-getting-started) +6. [Design](#design) + 1. [Architecture](#architecture) + 2. [UI Component](#ui-component) + 3. [Logic Component](#logic-component) + 4. [Model Component](#model-component) + 5. [AnalyticModel Component](#analyticmodel-component) + 6. [Storage Component](#storage-component) +7. [Implementation](#implementation) + 1. [Expense Features](#implemented-add-expense-feature) + 1. [Add Expense Feature](#implemented-add-expense-feature) + 2. [Delete Expense Feature](#implemented-delete-expense-feature) + 3. [Edit Expense Feature](#implemented-edit-expense-feature) + 4. [List Feature](#implemented-list-feature) + + 2. [RecurringExpenseManager Features](#implemented-add-recurringexpensemanager-feature) + 1. [Add RecurringExpenseManager Feature](#implemented-add-recurringexpensemanager-feature) + 2. [Delete RecurringExpenseManager Feature](#implemented-delete-recurringexpensemanager-feature) + 3. [Edit RecurringExpenseManager Feature](#implemented-edit-recurringexpensemanager-feature) + 4. [List RecurringExpenseManager Feature](#implemented-list-recurringexpensemanager-feature) + + 3. [Category Features](#implemented-add-category-feature) + 1. [Add Category Feature](#implemented-add-category-feature) + 2. [Delete Category Feature](#implemented-delete-category-feature) + 3. [Edit Category Feature](#implemented-edit-category-feature) + 4. [List Category Feature](#implemented-list-category-feature) + + 4. [Recurring Expense Feature](#implemented-recurring-expense-feature) + 5. [Budget Feature](#implemented-budget-feature) + 6. [Expense Statistics Feature](#implemented-expense-statistics-feature) + 7. [Autocomplete Feature](#implemented-category-autocomplete-feature) +8. [Documentation, logging, testing, configuration, dev-ops](#documentation-logging-testing-configuration-dev-ops) +9. [Appendix: Requirements](#appendix-requirements) + 1. [Product Scope](#product-scope) + 2. [User Stories](#user-stories) + 3. [Use Cases](#use-cases) + 4. [Non-functional requirements](#non-functional-requirements) + 5. [Glossary](#glossary) +10. [Appendix: Instructions for manual testing](#appendix-instructions-for-manual-testing) + 1. [Launch and Shut Down](#launch-and-shutdown) + 2. [Saving Data](#saving-data) +11. [Planned Enhancements](#planned-enhancements) +12. [Effort](#effort) + +--- + +## **FastTrack** + +FastTrack is an easy-to-use financial management desktop application designed for NUS SoC undergraduate students who are living on a tight budget. With a combination of a Command Line Interface (CLI) and Graphical User Interface (GUI), our app provides a user-friendly and efficient way to track your expenses and manage your finances. + +--- + +## **Purpose of this guide** + +The purpose of this guide is to give you a comprehensive insight for developing and maintaining FastTrack. + +If you are a developer, this guide will give you an overview of the high-level [design](#design) and architecture of FastTrack. It also delves into the [implementation](#implementation) and design considerations of FastTrack features, allowing you to learn the ins and outs of FastTrack in no time. If you are lost now, you can start by first looking at the [set-up](#setting-up-getting-started) portion. + +If you like to know more about the motivation behind FastTrack, checkout the [requirements](#appendix-requirements) section where we cover the [product scope](#product-scope) as well as the [user stories](#user-stories) and [use cases](#use-cases). + +--- + +## How to use this guide + +Here are some notations used in this guide. + +### Format + +- `Command` is used to label commands and components. +- {Placeholder} are used to label placeholders. +- [Optional], square brackets are used to notate optional fields. +- :information_source: **Note** is used to provide additional information that you should know. + +--- ## **Acknowledgements** -* {list here sources of all reused/adapted ideas, code, documentation, and third-party libraries -- include links to the original source as well} +- This project is based on the [AddressBook-Level3](https://github.com/se-edu/addressbook-level3) project created by the [SE-EDU initiative](https://se-education.org/) +- Libraries used: + - [JavaFX](https://openjfx.io/) + - [Jackson](https://github.com/FasterXML/jackson) + - [JUnit5](https://github.com/junit-team/junit5) --------------------------------------------------------------------------------------------------------------------- +--- ## **Setting up, getting started** Refer to the guide [_Setting up and getting started_](SettingUp.md). --------------------------------------------------------------------------------------------------------------------- +--- ## **Design** +This section gives you an overview of the different components of FastTrack and how they interact with each other. +
:bulb: **Tip:** The `.puml` files used to create diagrams in this document can be found in the [diagrams](https://github.com/se-edu/addressbook-level3/tree/master/docs/diagrams/) folder. Refer to the [_PlantUML Tutorial_ at se-edu/guides](https://se-education.org/guides/tutorials/plantUml.html) to learn how to create and edit diagrams. +
### Architecture - +The **_Architecture Diagram_** given below explains the high-level design of the FastTrack and how each component is connected. -The ***Architecture Diagram*** given above explains the high-level design of the App. - -Given below is a quick overview of main components and how they interact with each other. + **Main components of the architecture** -**`Main`** has two classes called [`Main`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/Main.java) and [`MainApp`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/MainApp.java). It is responsible for, -* At app launch: Initializes the components in the correct sequence, and connects them up with each other. -* At shut down: Shuts down the components and invokes cleanup methods where necessary. +**`Main`** has two classes called [`Main`](https://github.com/AY2223S2-CS2103T-W09-2/tp/blob/master/src/main/java/fasttrack/Main.java) and [`MainApp`](https://github.com/AY2223S2-CS2103T-W09-2/tp/blob/master/src/main/java/fasttrack/MainApp.java). It is responsible for, -[**`Commons`**](#common-classes) represents a collection of classes used by multiple other components. +- At app launch: Initializes the components in the correct sequence, and connects them up with each other. +- At shut down: Shuts down the components and invokes cleanup methods where necessary. -The rest of the App consists of four components. +[**`Commons`**](#common-classes) represents a collection of classes used by multiple other components. -* [**`UI`**](#ui-component): The UI of the App. -* [**`Logic`**](#logic-component): The command executor. -* [**`Model`**](#model-component): Holds the data of the App in memory. -* [**`Storage`**](#storage-component): Reads data from, and writes data to, the hard disk. +FastTrack also consists of five other components. +- [**`UI`**](#ui-component): The UI of the App. +- [**`Logic`**](#logic-component): The command executor. +- [**`Model`**](#model-component): Holds the data of the App in memory. +- [**`AnalyticModel`**](#analyticmodel-component): Holds the data and outputs statistics based on the data in memory. +- [**`Storage`**](#storage-component): Reads data from, and writes data to, the hard disk. **How the architecture components interact with each other** -The *Sequence Diagram* below shows how the components interact with each other for the scenario where the user issues the command `delete 1`. +The _Sequence Diagram_ below shows how the components interact with each other for the scenario where the user issues the command `delete 1`. + - + -Each of the four main components (also shown in the diagram above), -* defines its *API* in an `interface` with the same name as the Component. -* implements its functionality using a concrete `{Component Name}Manager` class (which follows the corresponding API `interface` mentioned in the previous point. +Each of the 5 main components (also shown in the diagram above), + +- defines its _API_ in an `interface` with the same name as the Component. +- implements its functionality using a concrete `{Component Name}Manager` class (which follows the corresponding API `interface` mentioned in the previous point.) For example, the `Logic` component defines its API in the `Logic.java` interface and implements its functionality using the `LogicManager.java` class which follows the `Logic` interface. Other components interact with a given component through its interface rather than the concrete class (reason: to prevent outside component's being coupled to the implementation of a component), as illustrated in the (partial) class diagram below. @@ -69,187 +154,815 @@ The sections below give more details of each component. ### UI component -The **API** of this component is specified in [`Ui.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/ui/Ui.java) +This component is responsible for displaying and interacting with users of FastTrack through the GUI. + +The **API** of this component is specified in [`Ui.java`](https://github.com/AY2223S2-CS2103T-W09-2/tp/blob/master/src/main/java/fasttrack/ui/Ui.java) + -![Structure of the UI Component](images/UiClassDiagram.png) + -The UI consists of a `MainWindow` that is made up of parts e.g.`CommandBox`, `ResultDisplay`, `PersonListPanel`, `StatusBarFooter` etc. All these, including the `MainWindow`, inherit from the abstract `UiPart` class which captures the commonalities between classes that represent parts of the visible GUI. + +The UI consists of a `MainWindow` that is made up of the following parts. + +- `CategoryListPanel` + - `CategoryCard` +- `ExpenseListPanel` + - `ExpenseCard` +- `RecurringExpensePanel` + - `RecurringExpenseCard` +- `StatisticsPanel` +- `CommandBox` + - `SuggestionListPanel` + - `SuggestionCard` +- `ResultDisplay` + - `ResultsHeader` + - `ResultsDetails` + +All these, including the `MainWindow`, inherit from the abstract `UiPart` class which captures the commonalities between classes that represent parts of the visible GUI. The `UI` component uses the JavaFx UI framework. The layout of these UI parts are defined in matching `.fxml` files that are in the `src/main/resources/view` folder. For example, the layout of the [`MainWindow`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/ui/MainWindow.java) is specified in [`MainWindow.fxml`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/resources/view/MainWindow.fxml) The `UI` component, -* executes user commands using the `Logic` component. -* listens for changes to `Model` data so that the UI can be updated with the modified data. -* keeps a reference to the `Logic` component, because the `UI` relies on the `Logic` to execute commands. -* depends on some classes in the `Model` component, as it displays `Person` object residing in the `Model`. +- executes user commands using the `Logic` component. +- listens for changes to `Model` data so that the UI can be updated with the modified data. +- keeps a reference to the `Logic` component, because the `UI` relies on the `Logic` to execute commands. +- depends on some classes in the `Model` component, as it displays `Person` object residing in the `Model`. ### Logic component -**API** : [`Logic.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/logic/Logic.java) +**API** : [`Logic.java`](https://github.com/AY2223S2-CS2103T-W09-2/tp/blob/master/src/main/java/fasttrack/logic/Logic.java) + +Here's a (partial) class diagram of the `Logic` component, to help guide you along on how it works: -Here's a (partial) class diagram of the `Logic` component: -How the `Logic` component works: -1. When `Logic` is called upon to execute a command, it uses the `AddressBookParser` class to parse the user command. -1. This results in a `Command` object (more precisely, an object of one of its subclasses e.g., `AddCommand`) which is executed by the `LogicManager`. -1. The command can communicate with the `Model` when it is executed (e.g. to add a person). + +#### **How the `Logic` component works:** + +1. When `Logic` is called upon to execute a command, it uses the `ExpenseTrackerParser` class to parse the user command. +1. This results in a `Command` object (more precisely, an object of one of its subclasses' subclass e.g., `AddCategoryCommand`, which implements `AddCommand`) which is executed by the `LogicManager`. +1. The command can communicate with the `Model` when it is executed (e.g. to add a category). 1. The result of the command execution is encapsulated as a `CommandResult` object which is returned back from `Logic`. -The Sequence Diagram below illustrates the interactions within the `Logic` component for the `execute("delete 1")` API call. +The Sequence Diagram below illustrates the interactions within the `Logic` component for the `execute("delcat 1")` API call. + ![Interactions Inside the Logic Component for the `delete 1` Command](images/DeleteSequenceDiagram.png) +
:information_source: **Note:** The lifeline for `DeleteCommandParser` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram.
Here are the other classes in `Logic` (omitted from the class diagram above) that are used for parsing a user command: + -How the parsing works: -* When called upon to parse a user command, the `AddressBookParser` class creates an `XYZCommandParser` (`XYZ` is a placeholder for the specific command name e.g., `AddCommandParser`) which uses the other classes shown above to parse the user command and create a `XYZCommand` object (e.g., `AddCommand`) which the `AddressBookParser` returns back as a `Command` object. -* All `XYZCommandParser` classes (e.g., `AddCommandParser`, `DeleteCommandParser`, ...) inherit from the `Parser` interface so that they can be treated similarly where possible e.g, during testing. + +#### **How the parsing works:** + +- When called upon to parse a user command, the `ExpenseTrackerParser` class creates an `XYZCommandParser` (`XYZ` is a placeholder for the specific command name e.g., `AddCategoryParser`) which uses the other classes shown above to parse the user command and create a `XYZCommand` object (e.g., `AddCategoryCommand`) which the `ExpenseTrackerParser` returns back as a `Command` object. +- All `XYZCommandParser` classes (e.g., `AddCategoryParser`, `DeleteCategoryParser`, ...) inherit from the `Parser` interface so that they can be treated similarly where possible e.g, during testing. ### Model component -**API** : [`Model.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/model/Model.java) - +**API** : [`Model.java`](https://github.com/AY2223S2-CS2103T-W09-2/tp/blob/master/src/main/java/fasttrack/model/Model.java) + + The `Model` component, -* stores the address book data i.e., all `Person` objects (which are contained in a `UniquePersonList` object). -* stores the currently 'selected' `Person` objects (e.g., results of a search query) as a separate _filtered_ list which is exposed to outsiders as an unmodifiable `ObservableList` that can be 'observed' e.g. the UI can be bound to this list so that the UI automatically updates when the data in the list change. -* stores a `UserPref` object that represents the user’s preferences. This is exposed to the outside as a `ReadOnlyUserPref` objects. -* does not depend on any of the other three components (as the `Model` represents data entities of the domain, they should make sense on their own without depending on other components) +- contains the expense tracker data i.e., all `Category` objects (which are contained in a `UniqueCategoryList` object), which is pulled from the `ReadOnlyExpenseTracker` instance. +- contains the currently 'selected' `Category` objects (e.g., results of a search query) as a separate _filtered_ list which is exposed to outsiders as an unmodifiable `ObservableList` that can be + 'observed' e.g. the UI can be bound to this list so that the UI automatically updates when the data in the list change. +- contains the expense tracker data i.e., all `Expense` objects (which are contained in a `ExpenseList` object), which is pulled from the `ReadOnlyExpenseTracker` instance. +- contains the currently 'selected' `Expense` objects (e.g., results of a search query) as a separate _filtered_ list which is exposed to outsiders as an unmodifiable `ObservableList` that can be + 'observed' e.g. the UI can be bound to this list so that the UI automatically updates when the data in the list change. +- does not depend on any of the other four components (as the `Model` represents data entities of the domain, they should make sense on their own without depending on other components) -
:information_source: **Note:** An alternative (arguably, a more OOP) model is given below. It has a `Tag` list in the `AddressBook`, which `Person` references. This allows `AddressBook` to only require one `Tag` object per unique tag, instead of each `Person` needing their own `Tag` objects.
+### AnalyticModel component - +**API** : [`AnalyticModel.java`](https://github.com/AY2223S2-CS2103T-W09-2/tp/blob/master/src/main/java/fasttrack/model/AnalyticModel.java) -
+The `AnalyticModel` component, + +- contains the expense tracker data (i.e. all `Expense` and `Category` objects, which are respectively contained in `ExpenseList` and `UniqueCategoryList` objects), which are pulled from the `ReadOnlyExpenseTracker` instance. +- calculates statistics based on the expense tracker data available. +- listens to any changes made to expense tracker data (by attaching listeners to the `ExpenseList`), thus dynamically updating statistics. ### Storage component -**API** : [`Storage.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/storage/Storage.java) +**API** : [`Storage.java`](https://github.com/AY2223S2-CS2103T-W09-2/tp/blob/master/src/main/java/fasttrack/storage/Storage.java) - + + The `Storage` component, -* can save both address book data and user preference data in json format, and read them back into corresponding objects. -* inherits from both `AddressBookStorage` and `UserPrefStorage`, which means it can be treated as either one (if only the functionality of only one is needed). -* depends on some classes in the `Model` component (because the `Storage` component's job is to save/retrieve objects that belong to the `Model`) + +- can save both expense tracker data and user preference data in json format, and read them back into corresponding objects. +- inherits from `ExpenseTrackerStorage` +- depends on some classes in the `Model` component (because the `Storage` component's job is to save/retrieve objects that belong to the `Model`) ### Common classes Classes used by multiple components are in the `seedu.addressbook.commons` package. --------------------------------------------------------------------------------------------------------------------- +--- ## **Implementation** This section describes some noteworthy details on how certain features are implemented. -### \[Proposed\] Undo/redo feature +### \[Implemented\] Add Expense feature -#### Proposed Implementation +#### **Command Format** -The proposed undo/redo mechanism is facilitated by `VersionedAddressBook`. It extends `AddressBook` with an undo/redo history, stored internally as an `addressBookStateList` and `currentStatePointer`. Additionally, it implements the following operations: +`add n/EXPENSE_NAME c/CATEGORY_NAME p/PRICE [d/DATE]`, whereby the date is an optional field. -* `VersionedAddressBook#commit()` — Saves the current address book state in its history. -* `VersionedAddressBook#undo()` — Restores the previous address book state from its history. -* `VersionedAddressBook#redo()` — Restores a previously undone address book state from its history. +#### **What is this feature for?** -These operations are exposed in the `Model` interface as `Model#commitAddressBook()`, `Model#undoAddressBook()` and `Model#redoAddressBook()` respectively. +The `add` command enables users to add an `Expense` to the `ExpenseTracker`. -Given below is an example usage scenario and how the undo/redo mechanism behaves at each step. +### **Sequence of action** -Step 1. The user launches the application for the first time. The `VersionedAddressBook` will be initialized with the initial address book state, and the `currentStatePointer` pointing to that single address book state. +To aid you in understanding how exactly the `add` command works, here is a list of steps illustrating what occurs when [`LogicManager#execute()` is invoked](#logic-component): -![UndoRedoState0](images/UndoRedoState0.png) +We will be using the user input `add n/Milk p/4.50 c/Groceries` as an example. -Step 2. The user executes `delete 5` command to delete the 5th person in the address book. The `delete` command calls `Model#commitAddressBook()`, causing the modified state of the address book after the `delete 5` command executes to be saved in the `addressBookStateList`, and the `currentStatePointer` is shifted to the newly inserted address book state. +1. The user inputs `add n/Milk p/4.50 c/Groceries` in the command box. -![UndoRedoState1](images/UndoRedoState1.png) +2. The input is then [parsed](#logic-component) and a `AddExpenseCommand` object is created using the given fields. -Step 3. The user executes `add n/David …​` to add a new person. The `add` command also calls `Model#commitAddressBook()`, causing another modified address book state to be saved into the `addressBookStateList`. +3. `AddExpenseCommand#execute()` is called, which takes in the currently-running instance of `Model` and adds the `Expense` used to instantiate the `AddExpenseCommand` through the `Model#addExpense()` method, which adds the new `Expense` object to the list of current `Expense` objects. Note that if the newly instantiated `Expense` has a `Category` that does not match any existing `Category` objects, the `Expense` object's `Category` will be added to the `ExpenseTracker` as a new `Category`. -![UndoRedoState2](images/UndoRedoState2.png) +4. A `CommandResult` instance is then returned, with feedback that the `Expense` was successfully logged. -
:information_source: **Note:** If a command fails its execution, it will not call `Model#commitAddressBook()`, so the address book state will not be saved into the `addressBookStateList`. +:information_source: **Note**: -
+- For step 2, if the user does not have any arguments, the `AddExpenseCommand` object will NOT be created! -Step 4. The user now decides that adding the person was a mistake, and decides to undo that action by executing the `undo` command. The `undo` command will call `Model#undoAddressBook()`, which will shift the `currentStatePointer` once to the left, pointing it to the previous address book state, and restores the address book to that state. +A sequence diagram is provided as follows, which matches the list of steps mentioned above: -![UndoRedoState3](images/UndoRedoState3.png) -
:information_source: **Note:** If the `currentStatePointer` is at index 0, pointing to the initial AddressBook state, then there are no previous AddressBook states to restore. The `undo` command uses `Model#canUndoAddressBook()` to check if this is the case. If so, it will return an error to the user rather -than attempting to perform the undo. + -
-The following sequence diagram shows how the undo operation works: +### \[Implemented\] Delete Expense feature -![UndoSequenceDiagram](images/UndoSequenceDiagram.png) +#### **Command Format** -
:information_source: **Note:** The lifeline for `UndoCommand` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram. +`delete INDEX` -
+#### **What is this feature for?** -The `redo` command does the opposite — it calls `Model#redoAddressBook()`, which shifts the `currentStatePointer` once to the right, pointing to the previously undone state, and restores the address book to that state. +The `delete` command enables users to delete an `Expense` from the `ExpenseTracker`. -
:information_source: **Note:** If the `currentStatePointer` is at index `addressBookStateList.size() - 1`, pointing to the latest address book state, then there are no undone AddressBook states to restore. The `redo` command uses `Model#canRedoAddressBook()` to check if this is the case. If so, it will return an error to the user rather than attempting to perform the redo. +### **Sequence of action** -
+To aid you in understanding how exactly the `delete` command works, here is a list of steps illustrating what occurs when [`LogicManager#execute()` is invoked](#logic-component): + +We will be using the user input `delete 1` as an example. + +1. The user inputs `delete 1` in the command box. + +2. The input is then [parsed](#logic-component) and a `DeleteExpenseCommand` object is created using the given fields. + +3. `DeleteExpenseCommand#execute()` is called, which takes in the currently-running instance of `Model`, retrieves the `Expense` object at the specified `INDEX`, and deletes it from the underlying list of `Expense` objects of the `Model` instance using `Model#deleteExpense()`. + +4. A `CommandResult` instance is then returned, with feedback that the `Expense` was successfully deleted. + +:information_source: **Note**: + +- At step 2, if input is detected as invalid, an error will be shown on the screen and the sequence of action is terminated. + +A sequence diagram is provided as follows, which matches the list of steps mentioned above: + + + + + +### \[Implemented\] Edit Expense feature + +#### **Command Format** + +`edexp INDEX [c/CATEGORY_NAME] [n/EXPENSE_NAME] [p/PRICE] [d/DATE]`, whereby all fields except the `INDEX` field are optional, BUT at least one of the optional fields +must be used (Otherwise, there is no purpose for calling this command). + +#### **What is this feature for?** + +The `edexp` command enables users to edit an `Expense` in the `ExpenseTracker`. + +### **Sequence of action** + +To aid you in understanding how exactly the `edexp` command works, here is a list of steps illustrating what occurs when [`LogicManager#execute()` is invoked](#logic-component): + +We will be using the user input `edexp 1 n/Chicken` as an example, whereby the original `Expense` object has a `EXPENSE_NAME` of `Milk`. + +1. The user inputs `edexp 1 n/Chicken` in the command box. + +2. The input is then [parsed](#logic-component) and a `EditExpenseCommand` object is created using the given fields. + +3. The `LogicManager#execute()` function causes `EditExpenseCommand#execute()` to be called, which takes in the currently-running instance of `Model`, retrieves the `Expense` object at the specified `INDEX` from the `FilteredList` of the `Model` instance, and instantiates a new `Expense` object that has the same fields as the retrieved `Expense` object except for the `EXPENSE_NAME`. Note that if the newly instantiated `Expense` has a `Category` that does not match any existing `Category` objects, the `Expense` object's `Category` will NOT be added to the `ExpenseTracker` as a new `Category`, and an error indicating that no such `CATEGORY_NAME` exists will pop up. + +4. The newly-instantiated `Expense` object with the updated `EXPENSE_NAME`, namely `Chicken`, will then replace the retrieved `Expense` object at the specified `INDEX` in the `ExpenseList` using `Model#setExpense()`. + +5. A `CommandResult` instance is then returned, with feedback that the `Expense` was successfully edited. + +:information_source: **Note**: + +- At step 2, if the input is detected as invalid (either index is invalid or no arguments provided other than index), a matching error will be shown on the screen and the sequence of action is terminated. +- At step 3, if the user provides a category to edit to, and it is found that there is no such category in FastTrack, an error will be shown and the sequence of action is terminated. + +A sequence diagram is provided as follows, which matches the list of steps mentioned above: + + + + + + + + +### **Design Considerations** + +**Aspect: How the expense is edited**: + +- **Alternative 1 (Current choice):** Retrieve the specified `Expense`, instantiate a new `Expense` with specified edited fields, and replace the retrieved `Expense` in the `ExpenseList`. + + - Pros: As the UI implementation requires listening in on the `ObservableList`, replacing the previous `Expense` object with a new `Expense` object makes the live-refreshing of our UI much more intuitive. + - Cons: There is an additional `Expense` object being instantiated to replace the previous `Expense` object. + +- **Alternative 2:** Retrieve the specified `Expense` and use setter methods to set the specified fields to be edited. + - Pros: No additional `Expense` object needs to be instantiated, and it is easier to simply set the fields. + - Cons: The listeners for the `ObservableList` only detect when there is an actual operation being done on the list, therefore setting fields will not cause the listeners to be updated, making our UI implementation much more complicated. + +### \[Implemented\] List feature + +#### **Command Format** + +`list [c/CATEGORY_NAME] [t/TIMEFRAME]`, whereby both `CATEGORY_NAME` and `TIMEFRAME` are optional. + +#### **What is this feature for?** + +The `list` command enables users to view all expenses within the expense tracker, and even allows them to view the expenses affiliated with a certain category or timeframe, or a combination of both. + +### **Sequence of action** + +To aid you in understanding how exactly the `list` command works, here is a list of steps illustrating what occurs when [`LogicManager#execute()` is invoked](#logic-component): + +We will be using the user input `list c/Groceries t/week` as an example. + +1. The user inputs `list c/Groceries t/week` in the command box. + +2. The input is then [parsed](#logic-component) and a `ListExpensesCommand` object is created using the given fields. + +3. The `LogicManager#execute()` function causes `ListExpensesCommand#execute()` to be called, which takes in the currently-running instance of `Model`, and applies the `ExpenseInCategoryPredicate` and `ExpenseInTimespanPredicate` sequentially onto the `Model` object's `FilteredList` by `Model#updateFilteredExpensesList()`. + +4. A `CommandResult` instance is then returned, with feedback of how many `Expenses` are currently listed under the current filters. + +:information_source: **Note**: + +- At step 2, if an invalid input is detected after `list` (e.g. `list xxxxxx`), an error will be shown and the sequence of action is terminated. + +A sequence diagram is provided as follows, which matches the list of steps mentioned above: + + + + + +### **Design Considerations** + +**Aspect: Whether to make `ListExpensesCommand` have multiple constructors or to make it take in `ExpenseInCategoryPredicate` and `ExpenseInTimespanPredicate` as `Optional` objects.** + +- **Alternative 1 (Current choice):** `ListExpensesCommand` takes in `Optional` and `Optional +1. The user inputs `addrec n/Broth c/Groceries p/3 sd/06/04/23 t/week` in the command box. + +2. The input is then [parsed](#logic-component) and a `AddRecurringExpenseCommand` object is created using the given fields. + +3. `AddRecurringExpenseCommand#execute()` is called, which takes in the currently-running instance of `Model`, and adds the previously instantiated `RecurringExpenseManager` to the `Model` object's current list of `RecurringExpenseManager` objects. Note that if the newly instantiated `RecurringExpenseManager` has a `Category` that does not match any existing `Category` objects, the `RecurringExpenseManager` object's `Category` will be added to the `ExpenseTracker` as a new `Category`. + +4. A `CommandResult` instance is then returned, with feedback of the `RecurringExpenseManager` object being successfully added. + +:information_source: **Note**: + +- At step 2, if any input is detected as missing or invalid, an error will be shown and the sequence of action is terminated. +- At the completion of step 3, due to the behavior of `RecurringExpenseManager`, expenses will be retroactively added if the start date up till current date (or end date if end date is earlier) sufficiently spans the timeframe frequency. + +An activity diagram is provided as follows, which matches and further elaborates on the list of steps mentioned above: + + + + +### **Design Considerations** + +**Aspect: Whether `Expense` objects should be retroactively added for `RecurringExpenseManager` objects that have `START_DATE` in the past.** + +- **Alternative 1 (Current choice):** `Expense` objects are retroactively added. + + - Pros: Allows users to easily add recurring expenses, even if they are in the past. + - Cons: If the user keys in the wrong `START_DATE`, they will need to take some time to manually remove all retroactively created `Expense` objects. + +- **Alternative 2:** `Expense` objects should not be retroactively added. + - Pros: Wrong input for the `START_DATE` will not cause a flood of retroactively created `Expense` objects to be added. + - Cons: Users might not want to take the time to accurately add the `Expense` objects in the past, which goes against our group's aim. + +### \[Implemented\] Delete RecurringExpenseManager feature + +#### **Command Format** + +`delrec INDEX` + +#### **What is this feature for?** + +The `delrec` command enables users to delete a `RecurringExpenseManager` from the `ExpenseTracker`. + +### **Sequence of action** + +To aid you in understanding how exactly the `delrec` command works, here is a list of steps illustrating what occurs when [`LogicManager#execute()` is invoked](#logic-component): + +We will be using the user input `delrec 1` as an example. + +1. The user inputs `delrec 1` in the command box. + +2. The input is then [parsed](#logic-component) and a `AddRecurringExpenseCommand` object is created using the given fields. + +3. `DeleteRecurringExpenseCommand#execute()` is called, which takes in the currently-running instance of `Model`, retrieves the `RecurringExpenseManager` object at the specified `INDEX`, and deletes it from the underlying list of `RecurringExpenseManager` objects of the `Model` instance using `Model#deleteRecurringExpense()`. + +4. The `RecurringExpenseManager` object, after being added to the list of other current `RecurringExpenseManager` objects, will retroactively add in `Expense` objects if the `START_DATE` and `FREQUENCY` make sense to do so. (e.g. The `START_DATE` is 03/02/2023, and the `FREQUENCY` is weekly, then, however many `Expense` objects will be generated up-to-date to try and help the user track recurring expenses accurately.) + +5. A `CommandResult` instance is then returned, with feedback that the `RecurringExpenseManager` object was successfully deleted. + +:information_source: **Note**: + +- At step 2, if the index is detected as invalid or missing, an error will be shown and the sequence of action is terminated. + +An activity diagram is provided as follows, which matches and further elaborates on the list of steps mentioned above: + + + + + +### **Design Considerations** + +**Aspect: Whether expenses generated by the `RecurringExpenseManager` object should also be deleted.** + +- **Alternative 1 (Current choice):** `Expense` objects generated by the `RecurringExpenseManager` object are NOT deleted upon the deletion of the `RecurringExpenseManager` object itself. + + - Pros: Makes more intuitive sense when a `RecurringExpenseManager` expires or is terminated by the user, past `Expense` objects should still remain as they have already been spent and allows for accurate tracking. + - Cons: If user unintentionally keys in wrong information for the `RecurringExpenseManager` that is irreversible in terms of generating `Expense` objects, it could end up taking a while for them to manually delete the `Expense` objects. + +- **Alternative 2:** `Expense` objects generated by the `RecurringExpenseManager` object are deleted upon the deletion of the `RecurringExpenseManager` object itself. + - Pros: If user unintentionally keys in wrong information for the `RecurringExpenseManager` that is generally irreversible in terms of generating `Expense` objects, it is easily undone by simply deleting the `RecurringExpenseManager` object. + - Cons: Users will not be able to accurately track past spendings as expired `RecurringExpenseManager` objects will delete affiliated `Expense` objects. + +### \[Implemented\] Edit RecurringExpenseManager feature + +#### **Command Format** + +`edrec INDEX [n/EXPENSE_NAME] [c/CATEGORY_NAME] [p/PRICE] [t/FREQUENCY] [ed/END_DATE]`, whereby all fields except for `INDEX` are optional, but at least one +of them must be specified (Otherwise, there is no point in editing). + +#### **What is this feature for?** + +The `edrec` command enables users to edit a `RecurringExpenseManager` in the `ExpenseTracker`. + +### **Sequence of action** + +To aid you in understanding how exactly the `edrec` command works, here is a list of steps illustrating what occurs when [`LogicManager#execute()` is invoked](#logic-component): + +We will be using the user input `edrec 1 n/Coconut Milk` as an example. + +1. The user inputs `edrec 1 n/Coconut Milk` in the command box. + +2. The input is then [parsed](#logic-component) and a `AddRecurringExpenseCommand` object is created using the given fields. + +3. `EditRecurringExpenseManagerCommand#execute()` is called, which takes in the currently-running instance of `Model`, retrieves the `RecurringExpenseManager` object at the specified `INDEX` from the `FilteredList` of the `Model` instance, and sets the `EXPENSE_NAME` field of the `RecurringExpenseManager` object found at the given `INDEX` to be `Coconut Milk`. + +4. A `CommandResult` instance is then returned, with feedback that the `RecurringExpenseManager` object was successfully edited. + +:information_source: **Note**: + +- At step 2, if the index provided is detected as invalid or if no arguments other than index are provided, an error will be shown and the sequence of action is terminated. +- At step 3, if the user provides a category to edit to, and it is found that there is no such category in FastTrack, an error will be shown and the sequence of action is terminated. + +An activity diagram is provided as follows, which matches and further elaborates on the list of steps mentioned above: + + + + + +### **Design Considerations** + +**Aspect: Whether the `RecurringExpenseManager` object should be replaced instead of having its fields set.** + +- **Alternative 1 (Current choice):** Retrieve the specified `RecurringExpenseManager` and use setter methods to set the specified fields to be edited. + + - Pros: Easy to simply retrieve the `RecurringExpenseManager` object and use setter for the fields. + - Cons: If any future developments require close listening to the `ObservableList` to be displayed in the UI, some refactoring might need to be done. + +- **Alternative 2:** Retrieve the specified `RecurringExpenseManager`, instantiate a new `RecurringExpenseManager` with specified edited fields, and replace the retrieved `RecurringExpenseManager` in the underlying `RecurringExpenseList`. + - Pros: Future developments which rely on the `ObservableList + + +### **Design Considerations** + +**Aspect: Whether to make `ListExpensesCommand` have multiple constructors or to make it take in `ExpenseInCategoryPredicate` and `ExpenseInTimespanPredicate` as `Optional` objects.** + +- **Alternative 1 (Current choice):** `ListExpensesCommand` takes in `Optional` and `Optional
+2. The input is then [parsed](#how-the-parsing-works) and a `AddCategoryCommand` object is created with the new `Category` object.

+3. `AddCategoryCommand#execute()` is called, which will trigger the model to add a category.

+4. `Model#addCategory()` is called, which further calls `ExpenseTracker#addCategory()`.

+5. `UniqueCategoryList#add()` is called to add the new category to FastTrack.

+6. A `CommandResult` is returned.

+ +:information_source: **Note:** + +- At step 3, if the new category has the same name as an existing category, a `CommandException` will be thrown and the sequence of action will be terminated. + +As the sequence diagram for this feature is highly similar in idea to that of the [add expense feature](#implemented-add-expense-feature) except for a difference in certain method names, there will be no sequence diagram provided. + +### \[Implemented\] Delete Category feature + +#### **Command format:** + +`delcat INDEX` where `INDEX` refers to the index of the category to be deleted when `lcat` is called. + +#### **What is the feature about:** + +This command allows user to delete a category of choice, expenses with the deleted category will have its category replaced with the `MiscellaneousCategory`. The category will be removed from the user's database. + +#### **Sequence of actions:** + +1. The user enters `delcat 1` in the command box.

+2. The input is then [parsed](#how-the-parsing-works) and a `DeleteCategoryCommand` object is created using the given `INDEX`.

+3. `DeleteCategoryCommand#execute()` is called, this will retrieve the category object to be deleted from the list of categories.

+4. This will call `Model#deleteCategory()`, which will further call `ExpenseTracker#removeCategory()`.

+5. `UniqueCategoryList#remove()` is called to remove the `Category` at index 2, and `ExpenseList#replaceDeletedCategory()` is called to replace all expenses with the deleted `Category` with `MiscellaneousCategory`.

+6. A `CommandResult` object is returned.

+ +:information_source: **Note:** + +- At step 2, if an invalid `INDEX` is given, a `CommandException` will be thrown and the sequence of action will be terminated. +- At step 3, if the `INDEX` given is out of range, a `CommandException` will be thrown and the sequence of action will be terminated. + +As the sequence diagram for this feature is highly similar in idea to that of the [delete expense feature](#implemented-delete-expense-feature) except for a difference in certain method names, there will be no sequence diagram provided. + +### \[Implemented\] Edit Category feature + +#### **Command format:** + +`edcat INDEX [c/NAME] [s/SUMMARY]` where `INDEX` refers to the index of the category to be edited when `lcat` is called, `NAME` is the new name of the category, `SUMMARY` is the new summary of the category. Note that either a `NAME` or `SUMMARY` must be present for the command to be executed. + +#### **What is the feature about:** + +This command allows user to edit a category of choice. We allow users to edit either the category's name, summary or both in this command. + +#### **Sequence of actions:** + +1. User enters `edcat 1 c/food s/expense on food` in the command box.

+2. The input is then [parsed](#how-the-parsing-works) and a `EditCategoryCommand` object is created using the given index `1`, new category name `food` and new summary `expense on food`.

+3. `EditCategoryCommand#execute()` is called. The category to edit is obtained from the `Model` and stored as `categoryToEdit`.

+4. The function checks that a new category name is present, thus it calls `UserDefinedCategory#setCategoryName()` on `categoryToEdit`, changing the name of the target category to the given new name.

+5. The function checks that a new category summary is present, thus is calls `UserDefinedCategory#setDescription()` on `categoryToEdit`, changing the summary of the target category with the given new summary.

+6. A `CommandResult` object is returned.

+ +:information_source: **Note:** + +- At step 2, if an invalid `INDEX` is given or **both** `c/NAME` and `s/SUMMARY` is missing, a `CommandException` will be thrown and the sequence of actions will be terminated. +- At step 4, if there is a category with the same name as the new name given, a `CommandException` is thrown and the sequence of actions will be terminated. + +As the sequence diagram for this feature is highly similar in idea to that of the [edit recurringexpensemanager feature](#implemented-edit-recurringexpensemanager-feature) except for a difference in certain method names, there will be no sequence diagram provided. #### Design considerations: -**Aspect: How undo & redo executes:** +**Aspect: How the category object is edited**: + +- **Alternative 1 (Current choice):** Directly retrieve and edit the currently-specified `Category` object. + + - Pros: No need to re-point all `Expense` objects currently affiliated with the `Category` object that is being edited. + - Cons: Mutates the state of the `Category` object, which might not be ideal if seeking immutability. -* **Alternative 1 (current choice):** Saves the entire address book. - * Pros: Easy to implement. - * Cons: May have performance issues in terms of memory usage. +- **Alternative 2 :** Retrieve the specified `Category` object's name and summary, and create a new `Category` object + that uses the same name and summary before replacing the required name or summary depending on user's arguments. + - Pros: Enforces immutability by replacing the previous `Category` object. + - Cons: There is now a need to re-direct all `Expense` objects affiliated with the previous `Category` object of interest. -* **Alternative 2:** Individual command knows how to undo/redo by - itself. - * Pros: Will use less memory (e.g. for `delete`, just save the person being deleted). - * Cons: We must ensure that the implementation of each individual command are correct. +### \[Implemented\] List Category feature -_{more aspects and alternatives to be added}_ +#### **Command Format** -### \[Proposed\] Data archiving +`lcat`. -_{Explain here how the data archiving feature will be implemented}_ +#### **What is this feature for?** +The `lcat` command enables users to view all `Category` objects within the expense tracker. --------------------------------------------------------------------------------------------------------------------- +### **Sequence of action** + +To aid you in understanding how exactly the `lcat` command works, here is a list of steps illustrating what occurs when [`LogicManager#execute()` is invoked](#logic-component): + +We will be using the user input `lcat` as an example. + +1. The user inputs `lcat` in the command box. + +2. The input is then [parsed](#logic-component) and a `ListCategoryCommand` object is created using the given fields. + +3. The `LogicManager#execute()` function causes `ListCategoryCommand#execute()` to be called, which takes in the currently-running instance of `Model`, and calls `Model#updateFilteredExpenseList()` with `PREDICATE_SHOW_ALL_EXPENSES`. + +4. A `CommandResult` instance is then returned, with feedback of how many `Category` objects are present in the expense tracker, as well as switching the screen to that of a list of the `Category` objects. + + +A sequence diagram is provided as follows, which matches the list of steps mentioned above: + + + +### **\[Implemented\] Recurring Expense feature:** + +#### **What is the feature about:** + +Since recurring expenses are prominent in today's society (e.g. Netflix, Transportation), therefore having a feature to allows FastTrack to regularly add recurring expenses automatically is a must. + +To implement this feature, 3 classes were added: + +1. [`RecurringExpenseManager`](#the-recurringexpensemanager-class) +2. [`RecurringExpenseList`](#the-recurringexpenselist-class) +3. [`RecurringExpenseType`](#the-recurringexpensetype-class) + +##### **The `RecurringExpenseManager` class:** + +The `RecurringExpenseManager` class is used as a generator of recurring expenses. When a user wants to add a recurring expense in FastTrack, the user can use the `addrec` command to create a new `RecurringExpenseManager` object. This object will have the following fields: + +- `expenseName` - Name of the recurring expense. +- `amount` - Unit price of the recurring expense. +- `category` - The category of the recurring expense. +- `startDate` - The date to start adding the recurring expenses. +- `[endDate]` - An optional ending date of the recurring expense. +- `nextExpenseDate` - The next date to charge the recurring expense. +- `recurringExpenseType` - The interval to charge the recurring expense (day, week, month, year). + +##### **The `RecurringExpenseList` class:** + +The `RecurringExpenseList` class works similar to the `ExpenseList` and `UniqueCategoryList` classes where is stores all the `RecurringExpenseManager` object created by the user. + +##### **The `RecurringExpenseType` class:** + +This is an enum class that stores the valid intervals (day, week, month, year) for a recurring expense. It also contains the `RecurringExpenseType#getNextExpenseDate()` method that calculates the next date given the interval and target date. + +#### **Sequence of actions:** + +Below is the sequence diagram when a user opens FastTrack: + + + +Pre-requisite: The user has added some `RecurringExpenseManager` into FastTrack and all `RecurringExpenseManager` objects have their `nextExpenseDate` updated. + +1. When the user reopens FastTrack, `Mainapp` will instantiate a new `Model` and `ExpenseTracker` object with the saved data. This will call `ExpenseTracker#resetData()`.

+2. `ExpenseTracker#generateRetroactiveExpenses()` is then called, this method goes through the user's `RecurringExpenseList` and calls the `RecurringExpenseManager#getExpenses()` method on each object.

+3. If the `nextExpenseDate` of the `RecurringExpenseManager` object is equal or before today's date, a new expense based on the parameters in the `RecurringExpenseManager` object will be added to a list until `nextExpenseDate` is after today's date or `endDate` if specified. The list is returned to the `ExpenseTracker`.

+4. The `ExpenseTracker` will then add each of the expenses in the returned list to FastTrack. + +#### **Design considerations:** + +##### Aspect: Making `RecurringExpenseManager` a class: + +- **Alternative 1:** Have a `RecurringExpenseManager` extend from `Expense`. Consist of another list of recurring expenses generated by the RecurringExpenseManager. + - Pros: Addition of 1 class only. Works similar to an `Expense` object with just an addition list to store the recurring expenses. + - Cons: Methods for adding a new recurring expense is longer as we need to traverse the entire expense list to locate the `RecurringExpenseManager` and add to the back of the list. Furthermore, the deletion of the `RecurringExpenseManager` also removes the existing recurring expenses.

+- **Alternative 2 (current choice):** Abstract `RecurringExpenseManager` to its own class, similar to category. + - Pros: Able to easily locate all `RecurringExpenseManager` objects that the user created, making addition of recurring expenses into FastTrack simpler and quicker. Deletion of the `RecurringExpenseManager` does not delete existing recurring expenses. + - Cons: Requires the addition of another `RecurringExpenseList` class to store all `RecurringExpenseManager` objects. Tedious to maintain and implement. + +**Alternative 2** was chosen as it would be more ideal to abide by the _Separation of Concerns_ principle. This allows proper separation of the generator class `RecurringExpenseManager` and the `Expense` class. + +### **\[Implemented\] Budget feature:** + +#### **Command format:** + +`set p/AMOUNT` where `AMOUNT` refers to the monthly budget amount. + +#### **What is the feature about:** + +This feature allows users to add a monthly budget to FastTrack. A weekly budget will be calculated for users by taking `AMOUNT` / 4. The `Budget` class is meant to be coupled with `AnalyticModel` to allow users to view helpful [statistics](#implemented-expense-statistics-feature) such as remaining budget etc. + +#### **Sequence of actions:** + +Below is the sequence diagram when a user uses the `set` command: + + + +1. User enters `set p/1000` in the command box.

+2. The input is then [parsed](#how-the-parsing-works) and a `SetBudgetCommand` object is created with a `Budget` object with an amount of `1000`.

+3. `SetBudgetCommand#execute()` is called, this will further call `Model#setBudget()` and `ExpenseTracker#setBudget()`. This will set the observable `simpleBudget` in `ExpenseTracker` to the given new `Budget`.

+4. A `CommandResult` object is returned.

+ +:information_source: **Note:** + +- At step 2, if an invalid amount is given, a `CommandException` will be thrown and the sequence of action will be terminated. + +#### **Design considerations:** + +##### Aspect: Making `Budget` a class: + +- **Alternative 1:** Make budget a field with `Double` type in `ExpenseTracker` rather than creating a new class. + - Pros: Easier to implement as there is no need for a creation of a class. + - Cons: Budget related calculations have to be done within the `ExpenseTracker` class, adding clutter. Modifications to the budget will also be more tedious as we have to locate these methods within `ExpenseTracker`.

+- **Alternative 2 (Current choice):** Make a new `Budget` class. + - Pros: Abstract all budget related operations to a class to maintain clean code. Modifications to budget related operations are also easier. + - Cons: Less convenient as it requires the creation of a new class. + +**Alternative 2** was chosen as it abides to the separation of concerns principle. This is to achieve better modularity and readability of the code base. + +### \[Implemented\] Expense Statistics Feature + +#### **What is the feature about:** + +FastTrack allows the user to view a summary of their expense statistics for both the current week and month. +These statistics are displayed on the expense summary screen on the right window of the application and are updated automatically everytime the user adds, edits or deletes an expense from FastTrack. + +The following statistics are calculated and displayed to the user. In FastTrack, the user is allowed to set only a monthly budget. A weekly budget is then defined as the value of the monthly budget divided by four (weeks). + +1. `Weekly Spending` - Total amount spent for the current week +2. `Monthly Spending` - Total amount spent for the current month +3. `Weekly Remaining` - Amount of weekly budget remaining for the current week +4. `Monthly Remaining` - Amount of monthly budget remaining for the current month +5. `Weekly % Change` - Percentage increase/decrease of weekly spending relative to the previous week +6. `Monthly % Change` - Percentage increase/decrease of monthly spending relative to the previous month +7. `Total Spent` - The total amount the user has spent to-date +8. `Budget Utilised` - The percentage of monthly budget that the user has utilised this month + +#### Implementation Details + +The current implementation of the expense summary feature requires consistent calculations of the user's expense statistics. As such, an `AnalyticModelManager`, which implements the `AnalyticModel` interface is used to keep track of all of these statistics. +It contains fields which keep track of each individual statistic as mentioned in the above list, as well as specific methods to perform new calculations and updates to each of these fields. + + +`AnalyticModelManager` requires an instance of `ExpenseTracker` to read and obtain the following unmodifiable, `ObservableList` objects containing data from FastTrack: + +1. `allExpenses`: an `ObservableList` of Expense objects representing all expenses in the expense tracker +2. `allCategories`: an `ObservableList` of Category objects representing all expense categories in the expense tracker +3. `simpleBudget`: a `ObjectProperty` object representing the monthly budget of the user. + +The fields contained within `AnalyticModelManager` are of type `DoubleProperty` which implement the `Observable` interface. This allows the `UI` to establish bindings to each property. +A binding is a mechanism of JavaFX allows for the establishment of relationships between variables. The `UI` observes each `DoubleProperty` for changes, and then updates the GUI automatically when it detects that the `DoubleProperty` has changed. + +Each `DoubleProperty` is updated using the respective calculation method, e.g. `AnalyticModelManager#getWeeklySpent()` for the `weeklySpent` property. A listener listens for changes in the `ObservableList` objects, `allExpenses` and `allCategories`. +Each time a change is detected, for example, when the user has deleted an expense or changed the amount of an expense, for each expense statistic, its corresponding calculation method is called, causing the corresponding property to be updated. This notifies the `UI` to update and display the new calculated expense statistics on the GUI. + +This method of implementation closely follows the _Observer Pattern_, which promotes loose coupling between the `UI` and the `AnalyticModelManager`. Bindings can also be added or removed dynamically, making any changes to the expense summary statistics more flexible and adaptable. + +#### Design Considerations + +**Aspect: Separation of Analytics and App Data**: + +- **Alternative 1 (Current choice):** Create two separate `ModelManager` components, one for managing analytics and another for managing app data. + - Pros: As analytics functions are read-only and do not require modifying the internal data of FastTrack, keeping the expense summary statistics in a separate component is ideal as it abides by the _Separation of Concerns_ principle. + - Cons: Less convenient to implement as a new class would be created. +- **Alternative 2 :** Store fields and methods for calculating expense summary statistics inside the existing `ModelManager` component. + - Pros: Easier to implement as the class already exists, there is no need to instantiate another `AnalyticModelManager` class. + - Cons: Will result in the current `ModelManager` being cluttered due to the presence of many differing getter and setter methods. Violates the _Separation of Concerns_ principle since the process of calculating statistics does not involve modifying the data within FastTrack, i.e. is purely read-only. + +**Alternative 1** was chosen as it would be more ideal to abide by the principle of _Separation of Concerns_. Moreover, as many developers were working on features that require direct modification of the `ModelManager` component, separating analytics into another `AnalyticModelManager` eliminates the possibility of merge conflicts. + +**Aspect: GUI update when user updates expenses in FastTrack**: + +- **Alternative 1:** Call methods to calculate and refresh the summary statistics after every user command. + - Pros: More convenient to implement since it is easy to ensure the GUI is always updated whenever the user enters a command + - Cons: Inefficient, as this would require tearing down and creating a new instance of each `UI` component in order to display the updated data. Redundant calculations would also need to be performed every single time the user enters a command that does not change the underlying expense data. +- **Alternative 2 (Current choice):** Use the _Observer Pattern_ to allow `UI` to update whenever the underlying data of FastTrack changes + - Pros: More efficient, since no redundant calculations are performed. The `AnalyticModelManager` also does not need a reference to the `UI` component to perform an update, which reduces the coupling between these classes. + - Cons: Was more time-consuming to implement, due to the need to learn about mechanisms like bindings and change listeners in JavaFX. + +**Alternative 2** was chosen as it was neater to implement and performs statistic calculations only when absolutely necessary, this preventing unnecessary wastage of computational resources. + +### \[Implemented\] Category Autocomplete feature + +The category autocompletion feature in FastTrack is implemented using two `UI` classes, namely the `SuggestionListPanel` and the `CommandBox`. + +The `SuggestionListPanel` is a JavaFX `UI` component responsible for displaying the suggestion list for the category autocomplete feature. It receives two parameters in its constructor, namely an `ObservableList` and a `CommandBox`. +The `ObservableList` contains all available categories in FastTrack, and the `CommandBox` is the `UI` component that contains a text field which allows users to enter commands, as well as for setting text to autofill. + +To filter the categories based on the user's input, the `SuggestionListPanel` makes use of a `FilteredList`. This filtered list is used as the data source for the `ListView` `UI` component of the `SuggestionListPanel`. +The `FilteredList` displays filtered categories based on whether the category name matches the user's input. + +Upon initialization, the `SuggestionListPanel` sets up autocomplete handlers for the suggestion list by calling the `SuggestionListPanel#initialiseAutocompleteHandlers()` method. This method registers listeners for focus changes, key presses, and autocomplete filters, which work together to ensure the autocomplete feature works responsively. + +When the user enters a command, they can trigger the autocomplete feature by typing `c/` followed by a few characters. These characters can then be used to filter the categories, and the most likely suggestions are displayed in the suggestion list. +To load the filter for autocompletion, the `SuggestionListPanel#loadAutocompleteFilter()` method within `SuggestionListPanel#initialiseAutocompleteHandlers()` sets up a listener for changes in the text field of the `CommandBox`. +If the user types `c/` to start entering a category name, the `SuggestionListPanel#getAutocompleteSuggestions()` method is called to retrieve the most likely suggestions based on the user's input so far. +This method filters the categories based on the user's input and updates the filtered categories as the items in the `ListView` `UI` component of the `SuggestionListPanel`. + +When the suggestion list is visible, the user can navigate through the suggestions using the `UP` and `DOWN` arrow keys. + +The `CommandBox#initialiseAutocompleteHandler()` method adds a key press event filter to the command text field that listens for the `UP` arrow key. + +When the user presses the `UP` arrow key, the focus is given to the `SuggestionListPanel` if it is visible (i.e. the user had typed in `c/`), and the `SuggestionListPanel#selectLastItemOnFocus()` method selects the last item in the suggestion list by default. When the user presses the `DOWN` arrow key, the `SuggestionListPanel#handleDownArrowKey()` method is called to check if the user is about to navigate out of the suggestion list and is responsible for returning the focus back to the `CommandBox`. + +To select a category from the suggestion list, the user can either navigate through the list using the arrow keys and press the `ENTER` key or press the `TAB` key to select the bottom-most suggestion in the list. +When the user makes a selection using the `ENTER` key, a callback function within the `SuggestionListPanel#addKeyPressListener()` method is called. This function updates the suggested text in the `CommandBox` by calling the `SuggestionListPanel#updateSuggestedText()` method, which sets the text in the `CommandBox` and subsequently hides the suggestion list. + +If the user makes the selection by pressing the `TAB` key, the `CommandBox#initialiseAutocompleteHandler()` method simulates a `UP` arrow key press followed by an `ENTER` key press, which also causes the first suggestion in the list to be selected. + +#### Design considerations: + +**Aspect: How the autocomplete feature should be implemented**: + +- **Alternative 1 (Current choice):** Add the autocomplete logic within the `SuggestionListPanel` and `CommandBox` classes itself + + - Pros: This design is more convenient and allows the methods within each class to focus specifically on their own behaviors, without explicit knowledge of the autocomplete feature. + - Cons: Currently, the `SuggestionListPanel` is tightly coupled to the `CommandBox` through its constructor parameters. This means that more modifications would need to be made if a new text input component was introduced. + +- **Alternative 2 :** Create a new `AutocompleteLogic` class which uses the _Observer Pattern_ to listen to and propagate changes across the `SuggestionListPanel` and `CommandBox` components. + - Pros: Enforces loose coupling by the _Observer Pattern_ + - Cons: Tedious to implement, and the added flexibility might be unnecessary since the autocomplete feature is not likely to be further extended upon. + +--- ## **Documentation, logging, testing, configuration, dev-ops** -* [Documentation guide](Documentation.md) -* [Testing guide](Testing.md) -* [Logging guide](Logging.md) -* [Configuration guide](Configuration.md) -* [DevOps guide](DevOps.md) +- [Documentation guide](Documentation.md) +- [Testing guide](Testing.md) +- [Logging guide](Logging.md) +- [Configuration guide](Configuration.md) +- [DevOps guide](DevOps.md) --------------------------------------------------------------------------------------------------------------------- +--- ## **Appendix: Requirements** @@ -257,73 +970,348 @@ _{Explain here how the data archiving feature will be implemented}_ **Target user profile**: -* has a need to manage a significant number of contacts -* prefer desktop apps over other types -* can type fast -* prefers typing to mouse interactions -* is reasonably comfortable using CLI apps +- NUS undergraduate students from the School of Computing +- Tech-savvy and able to type fast +- Comfortable using CLI apps +- Has to manage a large number of different general consumption and professional recurring expenses -**Value proposition**: manage contacts faster than a typical mouse/GUI driven app +**Value proposition**: +- Easy-to-use and allows students to log their daily expenses quickly and efficiently via a CLI +- Students can keep track of their spending habits with informative statistics +- FastTrack provides visual feedback and suggestions to help NUS students make more informed financial decisions ### User stories Priorities: High (must have) - `* * *`, Medium (nice to have) - `* *`, Low (unlikely to have) - `*` -| Priority | As a …​ | I want to …​ | So that I can…​ | -| -------- | ------------------------------------------ | ------------------------------ | ---------------------------------------------------------------------- | -| `* * *` | new user | see usage instructions | refer to instructions when I forget how to use the App | -| `* * *` | user | add a new person | | -| `* * *` | user | delete a person | remove entries that I no longer need | -| `* * *` | user | find a person by name | locate details of persons without having to go through the entire list | -| `* *` | user | hide private contact details | minimize chance of someone else seeing them by accident | -| `*` | user with many persons in the address book | sort persons by name | locate a person easily | +| Priority | As a …​ | I want to …​ | So that I can…​ | +| -------- | --------------------- | --------------------------------------------------------------------- | ----------------------------------------------------------------------------- | +| `* * *` | New user | See usage instructions | Refer to instructions when I forget how to use the app | +| `* * *` | NUS Computing Student | Add my own expense categories | Categorise my expenses with a high degree of customisation | +| `* * *` | NUS Computing Student | Delete an expense category | Remove categories that I no longer need | +| `* * *` | NUS Computing Student | List all categories | See all my categories at a glance | +| `* * *` | NUS Computing Student | Log an expense in a single command | Keep track of expenses in the system | +| `* * *` | NUS Computing Student | List all expenses in total | Get an overview of my expenses | +| `* * *` | NUS Computing Student | List all expenses by category | Analyse my expenses in each category | +| `* * *` | NUS Computing Student | Delete an expense | Remove expenses which I have keyed in incorrectly | +| `* * *` | NUS Computing Student | See a summary of my expenses | Analyse and correct my spending habits | +| `* * *` | NUS Computing Student | Keep track of Software-As-A-Service/application hosting subscriptions | Be prepared when a recurring expense is due soon | +| `* * *` | NUS Computing Student | Find an expense based on a keyword | Easily filter expenses based on their name | +| `* * *` | NUS Computing Student | Edit an expense | Update the details of an expense without deleting and creating a new one | +| `* * *` | NUS Computing Student | See how much I have spent in total in the list | Can see how much I have spent in total for any number of expenses in the list | +| `* *` | NUS Computing Student | See my expense to budget statistics | Ensure my expenses are within or have exceeded a predefined budget | +| `* *` | NUS Computing Student | List all expenses for a specified time frame | Track my expenses on a timely basis | +| `* *` | NUS Computing Student | Add descriptions to my categories containing additional details | Define my categories distinctly | +| `* *` | NUS Computing Student | See a percentage breakdown of my expenses | Be aware of what categories I should spend less on | +| `* *` | NUS Computing Student | Get feedback on how I can change my spending habits | Develop good financial habits | + +## Use cases + +(For all use cases below, the **System** is `FastTrack` and the **Actor** is the `user`, unless specified otherwise) + +**Precondition: The user has launched the FastTrack application** + +### Use case: UC1 - Add category -*{More to be added}* +**MSS** + +1. User requests to add a new category to FastTrack. +2. User enters the add command with the category name. +3. FastTrack adds the new category. +4. FastTrack responds with a success message indicating the category has been successfully added. + + Use case ends. + +**Extensions** + +- 2a. The user does not enter the required category name. + + - 2a1. FastTrack responds telling the user that a name is required and the command is invalid. + - 2a2. User enters add command with the category name. + - 2a3. Steps 2a1-2a2 are repeated until the data entered are correct. + + Use case resumes at step 3. + +- 2b. The category name already exists + + - 2b1. FastTrack informs the user that the category name has already been used and prompts the user for a different category name. + - 2b2. User enters add command with the category name. + - 2b3. Steps 2b1-2b2 are repeated until the data entered are correct. + + Use case resumes at step 3. + +### Use case: UC2 - Delete a Category + +**MSS** -### Use cases +1. User lists all categories using UC4. +2. User requests to delete a category from FastTrack. +3. User enters the delete command with the index i of the category to be deleted. +4. FastTrack deletes the category at index i. +5. FastTrack displays a success message to the user indicating the category has been successfully deleted. -(For all use cases below, the **System** is the `AddressBook` and the **Actor** is the `user`, unless specified otherwise) + Use case ends. -**Use case: Delete a person** +**Extensions** + +- 3a. The user selects an invalid category index (the index is out of bounds of the list) + + - 3a1. FastTrack displays an error message telling the user to key in a valid index. + - 3a2. User enters delete command with the category index. + - 3a3. Steps 3a1-3a2 are repeated until the data entered are correct. + + Use case resumes at step 4. + +### Use case: UC3 - Edit an Expense + +**MSS** + +1. The user lists all expenses using UC7. +2. The user selects the expense they want to edit from the list of expenses. +3. User keys in the command to edit the expense. +4. FastTrack responds with a success message indicating the expense has been successfully edited. + +**Extensions** + +- 2a. The user selects an expense that does not exist. + - 2a1. FastTrack displays an error message and does not allow the user to edit the expense. +- 2b. The user tries to save an expense with invalid or missing data. + - 2b1. FastTrack displays an error message indicating the fields that need to be corrected. + +**Precondition: The user has created at least one expense in the app** + +### Use case: UC4 - List all Categories **MSS** -1. User requests to list persons -2. AddressBook shows a list of persons -3. User requests to delete a specific person in the list -4. AddressBook deletes the person +1. User requests to list all categories. +2. FastTrack displays all categories. + +Use case ends. + +**Extensions** + +- 2a. The user does not have any categories. + - 2a1. FastTrack only displays the Misc category + +### Use case: UC5 - Add an expense + +**MSS** + +1. User wants to add an expense to be tracked. +2. User keys in the command to add an expense to be tracked. +3. FastTrack responds with a successfully added message. + +**Extensions** + +- 2a. User keys in information in wrong format. + + - 2a1. FastTrack returns an error, requesting that the user inputs information in the correct format. + - 2a2. User inputs information again. + - 2a3. Steps 2a1-2a2 are repeated until the information being input is of the correct format. + + Use case resumes from step 3. + +### Use case: UC6 - Delete an expense + +**MSS** + +1. User wants to delete an expense that has been tracked. +2. User uses UC7 (to list out all expenses currently added). +3. User keys in the command to delete the expense. +4. FastTrack responds with a successfully deleted message. + + Use case ends. + +**Extensions** + +- 3a. User keys in an invalid expense index. + + - 3a1. FastTrack returns an error, requesting that the user inputs the correct expense index. + - 3a2. User inputs information again. + - 3a3. Steps 3a1-3a2 are repeated until the expense index being input by the user is valid. + + Use case resumes from step 4. + +**MSS** + +### Use case: UC7 - List all expense + +**MSS** + +1. User requests to list all expense. +2. FastTrack displays all expenses added by user. + + Use case ends. + +### Use case: UC8 - List all expense in a given category + +**MSS** + +1. User requests to list all expense in a given category. +2. FastTrack displays all expenses in a given category added by user. + Use case ends. + +- 1a. User does not enter a category. + + - 1a1. FastTrack displays error message. + + Use case ends. + +- 1b. The given category is invalid. + + - 1b1. FastTrack displays an error message. + + Use case ends. + +### Use case: UC9 - List all expense in the past week + +**MSS** + +1. User requests to list all expense in the past week. +2. FastTrack displays all expenses added by user in the past week . + + Use case ends. + +### Use case: UC10 - List all expense in a given category in the past week + +**MSS** + +1. User requests to list all expense in a category in the past week. +2. FastTrack displays all expenses added by user in the category in the past week. + + Use case ends. + +### Use case: UC11 - Find an expense + +**MSS** + +1. User requests to find an expense. +2. FastTrack displays all expenses related to the keyword provided. Use case ends. +### Use case: UC12 - Clear all expenses from the expense log + +**MSS** + +1. User wants to wipe all currently-logged expenses. +2. User keys in the command to clear all logged expenses. + + Use case ends. + +### Use case: UC13 - Get Help within the app + +**MSS** + +1. User wants to check help for the commands to use FastTrack. +2. User keys in the command to get help. +3. FastTrack opens a pop-up window to show help for commands and a link to the User Guide. + + Use case ends. + +### Use case: UC14 - Exit from FastTrack + +**MSS** + +1. User wants to exit the application. +2. User keys in the command to exit the application. +3. FastTrack exits and is closed. + + Use case ends. + +### Use case: UC15 - Add a recurring expense + +**MSS** + +1. User wants to add a recurring expense to be tracked. +2. User keys in the command to add a recurring expense. +3. FastTrack responds with a successfully added message. + + Use case ends. + **Extensions** -* 2a. The list is empty. +* 2a. User keys in invalid information or wrongly-formatted information. + * 2a1. FastTrack returns an error, requesting that the user inputs information in the correct format. + * 2a2. User inputs information again. + * 2a3. Steps 2a1-2a2 are repeated until the information being input is of the correct format. - Use case ends. + Use case resumes from step 3. -* 3a. The given index is invalid. +### Use case: UC16 - List all recurring expense generators - * 3a1. AddressBook shows an error message. +**MSS** - Use case resumes at step 2. +1. User requests to list all recurring expense generators. +2. FastTrack displays all recurring expense generators added by user. -*{More to be added}* + Use case ends. + +### Use case: UC17 - Delete a recurring expense + +**MSS** + +1. User lists all recurring expense generators using UC16. +2. User requests to delete a recurring expense generator from FastTrack with the index of the recurring expense generator to be deleted. +3. FastTrack deletes the recurring expense generator indicated at the index. +4. FastTrack displays a success message to the user indicating that the recurring expense generator has been successfully deleted. + + Use case ends. + +**Extensions** + +* 2a. The user selects an invalid index. + * 2a1. FastTrack displays an error message telling the user to key in a valid index. + * 2a2. User re-enters the delete command with the index. + * 2a3. Steps 2a1-2a2 are repeated until the index provided is valid. + + Use case resumes from step 3. + +### Use case: UC18 - Edit a recurring expense + +**MSS** + +1. User lists all recurring expense generators using UC16. +2. User requests to edit a recurring expense generator from FastTrack with the index of the curring expense generator to be edited as well as the fields to be edited. +3. FastTrack edits the recurring expense generator indicated at the index. +4. FastTrack displays a success message to the user indicating that the recurring expense generator has been successfully edited. + + Use case ends. + +**Extensions** + +* 2a. The user selects an invalid index. + * 2a1. FastTrack displays an error message telling the user to key in a valid index. + * 2a2. User re-enters the edit command with the index. + * 2a3. Steps 2a1-2a2 are repeated until the index provided is valid. + + Use case resumes from step 3. + +* 2b. The user inputs invalid data (wrong format) + * 2b1. FastTrack displays an error message telling the user to key in information in correct format. + * 2b2. User re-enters the edit command with information. + * 2b3. Steps 2b1-2b2 are repeated until the information provided is of correct format. -### Non-Functional Requirements -1. Should work on any _mainstream OS_ as long as it has Java `11` or above installed. -2. Should be able to hold up to 1000 persons without a noticeable sluggishness in performance for typical usage. -3. A user with above average typing speed for regular English text (i.e. not code, not system admin commands) should be able to accomplish most of the tasks faster using commands than using the mouse. + Use case resumes from step 3. -*{More to be added}* +### Non-Functional Requirements + +1. Should work on any _mainstream OS_ as long as it has Java `11` or above installed. +2. Should be able to hold up to 1000 expenses without a noticeable sluggishness in performance for typical usage. +3. A user with above average typing speed for regular English text (i.e. not code, not system admin commands) should be able to accomplish most of the tasks faster using commands than using the mouse. +4. FastTrack should be designed in a modular, scalable manner to enable easy addition of new features in the future. +5. The code should be well-organized and well-documented to ensure ease of maintenance and debugging. +6. FastTrack should protect user data from unauthorized access or modification. +7. Any modification to the data will result in a prompt update to the user interface. ### Glossary -* **Mainstream OS**: Windows, Linux, Unix, OS-X -* **Private contact detail**: A contact detail that is not meant to be shared with others +- **Mainstream OS**: Windows, Linux, Unix, OS-X --------------------------------------------------------------------------------------------------------------------- +--- ## **Appendix: Instructions for manual testing** @@ -340,38 +1328,64 @@ testers are expected to do more *exploratory* testing. 1. Download the jar file and copy into an empty folder - 1. Double-click the jar file Expected: Shows the GUI with a set of sample contacts. The window size may not be optimum. + 1. Double-click the jar file Expected: Shows the GUI with a set of sample expenses and categories. The window size may not be optimum. 1. Saving window preferences 1. Resize the window to an optimum size. Move the window to a different location. Close the window. 1. Re-launch the app by double-clicking the jar file.
- Expected: The most recent window size and location is retained. + Expected: The most recent window size and location is retained. -1. _{ more test cases …​ }_ -### Deleting a person +### Saving data -1. Deleting a person while all persons are being shown +1. Dealing with missing/corrupted data files - 1. Prerequisites: List all persons using the `list` command. Multiple persons in the list. + 1. In the case whereby your data files end up corrupted, FastTrack will wipe all data and allow you to start over + with a clean slate. - 1. Test case: `delete 1`
- Expected: First contact is deleted from the list. Details of the deleted contact shown in the status message. Timestamp in the status bar is updated. - 1. Test case: `delete 0`
- Expected: No person is deleted. Error details shown in the status message. Status bar remains the same. +## Planned Enhancements +1. The current function of setting a budget does not properly take into account the occasion whereby the user actually wants to set a 0-dollar budget, and thus our team has opted to use a 0-dollar budget to be defined as the state of not having set a budget. +We plan to make 0-dollars no longer the state of the user not setting a budget, and instead, use another proper indicator to show that a user has not set his or her budget. - 1. Other incorrect delete commands to try: `delete`, `delete x`, `...` (where x is larger than the list size)
- Expected: Similar to previous. +2. The current expense statistics do not make it glaringly obvious that the user's budget has been exceeded, as it is only shown in plain words without any form of +colour-highlighting or ways to make the user notice it easily. We plan to make the indicator that one's budget has been exceeded very obvious for the user to notice by highlighting and +bolding the text. -1. _{ more test cases …​ }_ +3. The current method of taking in dates is unable to properly distinguish a proper date with reference to that of the days 29-31 of a month. (e.g. the 31st of February is a valid input for our current state, but we force the date to be the 28th of February in our application) +We plan to properly handle this behavior for the months whereby the 29th-31st days do not exist by properly throwing an error. -### Saving data +4. The current colour choices in the expense statistics are possibly counter-intuitive, as we have chosen the colour red in cases whereby the user has spent more in the +current month as compared to that of the previous month to indicate negative performance, but this might confuse users as the numbers shown are +x%, whereby x is the percentage point increase in spending +as compared to the previous month. The same applies for spending lesser as compared to the previous month but in green. We are planning to make it such that the +colour choice is more intuitive (Possibly swap the colour choices around). -1. Dealing with missing/corrupted data files - 1. _{explain how to simulate a missing/corrupted file, and the expected behavior}_ -1. _{ more test cases …​ }_ + +## Effort + +The difficulty of FastTrack as a project was quite high, but one that allowed us to learn a lot. As our group lacked experience in working with +a properly-structured team as well as a pre-existing codebase that had to be overhauled for our own purposes, we had to start from scratch to learn +how exactly commands and user inputs were handled by the pre-existing AB3 codebase. + +We faced numerous challenges in our journey to make FastTrack, not limited to: + +1. Learning how the codebase worked and how to restructure existing code. +2. How to design the structure of FastTrack so that the `Category`, `Expense` and `RecurringExpenseManager` made sense. +3. Abstracting out certain checks into proper methods in the correct classes so as to reduce violations of principles. +4. Testing of code. + +Our group reused much of what AB3 had in place, with the main bulk of reusability coming from AB3's parsing set-up and +the model as well as storage systems, which we found to be useful as they worked in quite an intuitive and robust manner after we took some +time to understand them. + +However, we faced major difficulties in attempting to modify AB3 to suit FastTrack, as we realized +that AB3 primarily dealt with `Person` objects, which were only one type of entity, while we had to deal with 3 kinds of entities as +mentioned above. This led to multiple problems in our implementations and debugging stages, and we had to race against time +on multiple occasions to thankfully, successfully resolve these issues. + +To sum it all up, our team did put in a significant amount of work to understand and mostly to successfully morph the existing AB3 +codebase into FastTrack, and there is no doubt that we have achieved what we set out to do, and more. diff --git a/docs/SettingUp.md b/docs/SettingUp.md index 275445bd551..bd0d9692c53 100644 --- a/docs/SettingUp.md +++ b/docs/SettingUp.md @@ -23,7 +23,7 @@ If you plan to use Intellij IDEA (highly recommended): 1. **Import the project as a Gradle project**: Follow the guide [_[se-edu/guides] IDEA: Importing a Gradle project_](https://se-education.org/guides/tutorials/intellijImportGradleProject.html) to import the project into IDEA.
:exclamation: Note: Importing a Gradle project is slightly different from importing a normal Java project. 1. **Verify the setup**: - 1. Run the `seedu.address.Main` and try a few commands. + 1. Run the `fasttrack.Main` and try a few commands. 1. [Run the tests](Testing.md) to ensure they all pass. -------------------------------------------------------------------------------------------------------------------- @@ -45,7 +45,7 @@ If you plan to use Intellij IDEA (highly recommended): 1. **Learn the design** - When you are ready to start coding, we recommend that you get some sense of the overall design by reading about [AddressBook’s architecture](DeveloperGuide.md#architecture). + When you are ready to start coding, we recommend that you get some sense of the overall design by reading about [FastTrack's architecture](DeveloperGuide.md#architecture). 1. **Do the tutorials** These tutorials will help you get acquainted with the codebase. diff --git a/docs/UserGuide.md b/docs/UserGuide.md index e7df68b01ea..28ae31fa356 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -1,193 +1,1041 @@ --- layout: page -title: User Guide +title: FastTrack User Guide + +--- + +--- + +
+
+ +# **Table of Contents** + +1. [Introduction to FastTrack](#introduction-to-fasttrack) +2. [Why You Should Use FastTrack](#why-you-should-use-fasttrack) +3. [Purpose of this guide](#purpose-of-this-guide) +4. [How to understand this guide](#understanding-this-guide) + 1. [Icons](#icons) +5. [Quick Start and Installation](#quick-start-and-installation) +6. [GUI Walkthrough](#graphical-user-interface-gui-walkthrough) +7. [Features](#features) + 1. [Command Syntax](#command-syntax) + 2. [Category Features](#category-features) + 3. [Expense Features](#expense-features) + 1. [One-Time Expenses](#one-time-expenses) + 2. [Recurring Expenses](#recurring-expenses) + 4. [General Features](#general-features) + 5. [Expense Statistics Feature](#expense-statistics-feature) +8. [Saving the data](#saving-the-data) +9. [Editing the data file (For Advanced Users)](#editing-the-data-file) +10. [Frequently Asked Questions](#frequently-asked-questions) + +--- + +# Introduction to FastTrack + +FastTrack is an easy-to-use **financial management desktop application** designed for NUS SoC undergraduate students who are living on a tight budget. + +With a combination of a Command Line Interface (CLI) and Graphical User Interface (GUI), our app provides a user-friendly and efficient way to track your expenses and manage your finances. + +FastTrack prioritizes speed and efficiency to save your precious time and money, so you have more resources to spend on the important things in life. + +![Ui](images/Ui.png) + +--- + +## Why you should use FastTrack + +**FastTrack** is an expense tracking app that helps computing students keep track of their expenses by providing a simple and convenient command-line interface. Here are some reasons why you should consider using FastTrack: + +1. **Simplicity:** FastTrack provides a _simple_ and _easy-to-use_ command-line interface that allows you to quickly add and track your expenses. This makes it ideal for computing students who prefer to use the command line to work with data.

+2. **Speed:** FastTrack prioritizes speed and efficiency. With its command-line interface and all commands being a simple and one-line, it skips the hassle of clicking through screens like other expense tracking apps. The entire interface is shown in one screen.

+3. **Convenience:** FastTrack can be used on any platform, including Windows, Mac, and Linux, making it convenient for computing students to track their expenses regardless of the platform they are using.

+4. **Customizable:** FastTrack is highly customizable, allowing you to tailor it to your specific needs. You can add categories, set budgets, add recurring expenses, and even see statistics on your expenses.

+5. **Security:** FastTrack is a locally hosted app that allows you to keep your expenses and financial information private. It does not require any personal information or financial details to use, ensuring that your information remains secure.

+6. **Free and Open Source:** FastTrack is a free and open-source app, meaning that it is available for download and use by anyone. + +--- + +# Purpose of this Guide + +This User Guide provides information on how to use FastTrack. It includes: + +- [Installation](#quick-start-and-installation) and setup of app +- Detailing [features](#features) of the app +- Usage of app and its commands +- Tips, tricks and warnings on usage of commands +- Troubleshooting tips +- Answering [Frequently Asked Questions](#frequently-asked-questions) + --- -AddressBook Level 3 (AB3) is a **desktop app for managing contacts, optimized for use via a Command Line Interface** (CLI) while still having the benefits of a Graphical User Interface (GUI). If you can type fast, AB3 can get your contact management tasks done faster than traditional GUI apps. +# Understanding this guide + +## Icons + +Throughout FastTrack's user guide, you may encounter unfamiliar symbols. This is a quick overview of what these symbols +mean and what to look out for. + +| **Icon** | **Meaning** | +| -------------------- | ----------- | +| :warning: | Warning | +| :information_source: | Information | +| :bulb: | Tip | + +#### Warning Box -* Table of Contents -{:toc} +
:warning: Warning: +Danger zone! Do pay attention to the information here carefully. Careless usage of this function may cause certain +things to not work as expected. +
+ +#### Information Box + +
:information_source: Info: +This provides additional useful information that may help you with using FastTrack's features. +
--------------------------------------------------------------------------------------------------------------------- +#### Tip Box -## Quick start +
:bulb: Tip: +This provides some quick and convenient hacks that you can use to optimize your experience with FastTrack. +
+ +--- + +## Quick start and Installation 1. Ensure you have Java `11` or above installed in your Computer. -1. Download the latest `addressbook.jar` from [here](https://github.com/se-edu/addressbook-level3/releases). +2. Download the latest `fastTrack.jar` [here](https://github.com/AY2223S2-CS2103T-W09-2/tp/releases). + +3. Drag the file into a folder you want to use as the _home folder_ for FastTrack. -1. Copy the file to the folder you want to use as the _home folder_ for your AddressBook. +4. Double-click the FastTrack JAR file to run the application. -1. Open a command terminal, `cd` into the folder you put the jar file in, and use the `java -jar addressbook.jar` command to run the application.
- A GUI similar to the below should appear in a few seconds. Note how the app contains some sample data.
- ![Ui](images/Ui.png) + A Graphical User Interface (pictured below) will appear. Note how the app contains some sample data.
-1. Type the command in the command box and press Enter to execute it. e.g. typing **`help`** and pressing Enter will open the help window.
+![Ui](images/UiStartup.PNG) + +6. Type a command in the command box and press Enter to execute it. e.g. typing **`help`** and pressing Enter will open the help window.
Some example commands you can try: - * `list` : Lists all contacts. + - `list` : Lists all expenses + + - `clear` : Clears the sample data + + - `add c/groceries n/milk p/4.50 d/14/2/23` : Adds an expense named `milk` to the expenses list with a price of $4.50 and a date of 14/02/2023 + + - `delete 3` : Deletes the 3rd expense shown in the current list + + - `exit` : Exits the app + +7. Refer to the [Features](#features) below for details of each command. + +--- + +# Graphical User Interface (GUI) Walkthrough - * `add n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01` : Adds a contact named `John Doe` to the Address Book. +The following diagrams highlight the different sections of the _Graphical User Interface (GUI)_ of FastTrack. - * `delete 3` : Deletes the 3rd contact shown in the current list. +![FastTrack GUI](images/demo/intro/fasttrack_labeled_1.png) +The **main display**. It displays all added expenses on the left, showing each expense's price, category and date added. - * `clear` : Deletes all contacts. +![FastTrack GUI](images/demo/intro/fasttrack_labeled_2.png) +The **Category display**. It shows all currently added Categories for expenses. This display is shown only after using the +command to list categories. - * `exit` : Exits the app. +![FastTrack GUI](images/demo/intro/fasttrack_labeled_3.png) +The **Recurring Expense display**. It shows all currently added Recurring Expenses. This display is shown only after using the +command to list recurring expenses. -1. Refer to the [Features](#features) below for details of each command. +| **FastTrack UI Part** | **Description** | +| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| One-time Expense Display | Displays the list of saved one-time expenses with filters applied (if any). This display occupies the _Main View_ section. | +| Category Display | Displays the list of saved categories, including the number of expenses associated with each category. This display occupies the _Main View_ section. | +| Recurring Expense Display | Displays the list of saved recurring expenses. This display occupies the _Main View_ section. | +| Results Display | Displays feedback from the application after entering a command, which can be used to indicate the outcome of the command. It provides guidance for the user, especially if a command has failed. | +| Command Box | A text input field where you can type in a command for FastTrack to execute. | +| Expense Summary Display | A visual display containing expense statistics (Refer to the feature [Expense Statistics](#expense-statistics-feature) below for details. | +| Toolbar | Contains buttons which allow you to access the user guide and exit from the application. | --------------------------------------------------------------------------------------------------------------------- +--- ## Features -
+The features of FastTrack can be divided into 4 groups, **Category Features**, **Expense Features**, **General Features** and **Expense Statistics Feature**. With these 4 groups in mind, remembering the different commands becomes extremely convenient, as each group contains mainly 4 types of operations - add, delete, edit and list! + +### Commands + +| [**Category**](#category-features) | [**One-Time Expenses**](#one-time-expenses) | [**Recurring Expenses**](#recurring-expenses) | [**General**](#general-features) | +| --------------------------------------------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------ | --------------------------------------------------- | +| [Add a category](#adding-a-category-addcat) | [Add an expense](#adding-an-expense-add) | [Add a recurring expense](#adding-a-recurring-expense-addrec) | [Set a budget](#setting-a-budget-set) | +| [Edit a category](#editing-a-category-edcat) | [Edit an expense](#editing-an-expense-edexp) | [Edit a recurring expense](#editing-a-recurring-expense-edrec) | [Category autocompletion](#category-autocompletion) | +| [Delete a category](#deleting-a-category-delcat) | [Delete an expense](#deleting-an-expense-delete) | [Delete a recurring expense](#deleting-a-recurring-expense-delrec) | [Clear all entries](#clearing-all-entries-clear) | +| [List categories](#listing-categories-lcat) | [Find an expense by keyword](#search-for-an-expense-by-keyword-find) | [List recurring expenses](#listing-recurring-expenses-lrec) | [Exit FastTrack](#exiting-fasttrack-exit) | +| [View category summary](#viewing-category-summary-sumcat) | [List expenses](#listing-expenses-list) | | [View help](#viewing-help-help) | + + +### Other Notable Features + +| [**Expense Statistics Feature**](#expense-statistics-feature) | +| ----------------------------------------------------------------------------------- | +| [Monthly spending statistic](#monthly-spending-statistic) | +| [Monthly remaining statistic](#monthly-remaining-statistic) | +| [Monthly percentage change statistic](#monthly-percentage-change-statistic) | +| [Weekly spending statistic](#weekly-spending-statistic) | +| [Weekly remaining statistic](#weekly-remaining-statistic) | +| [Weekly percentage change statistic](#weekly-percentage-change-statistic) | +| [Total spent statistic](#total-spent-statistic) | +| [Budget utilisation percentage statistic](#budget-utilisation-percentage-statistic) | + +## Command Syntax + +First-time users may have difficulty understanding the syntax described in the command instructions. + +If you are new to using a **Command Line Interface (CLI)**, we recommend reading this brief section before using FastTrack. +Understanding the **CLI** will help you enter commands more efficiently, which can save you time in the long run. + +In simple terms, the **Command Line Interface (CLI)** is a way to interact with FastTrack by typing in commands using just one line of text. This means you can add expenses quickly and easily. -**:information_source: Notes about the command format:**
+Here is a quick guide on how to read the syntax mentioned in the User Guide for using FastTrack's commands. -* Words in `UPPER_CASE` are the parameters to be supplied by the user.
- e.g. in `add n/NAME`, `NAME` is a parameter which can be used as `add n/John Doe`. +``` +command tag1/ PARAMETER_1 tag2/ PARAMETER_2 [tag3/ PARAMETER_3] +``` -* Items in square brackets are optional.
- e.g `n/NAME [t/TAG]` can be used as `n/John Doe t/friend` or as `n/John Doe`. +| Element | Format | Usage | +| ----------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `command` | Name of command
eg. `add`, `find` | Specifies the command to be executed. | +| `tag/` | Prefix for a field, followed by `/` | Specifies which field given input argument is for | +| `PARAMETER` | Words that are in UPPERCASE | Specifies user input for field specified by `tag/`

e.g.
In `add c/CATEGORY_NAME`, `CATEGORY_NAME` is a parameter, which the user decides is “groceries”. The final command will be entered as add `c/groceries`. | +| `[]` | Square brackets around `tag/` and `PARAMETER` | Indicates that field specified by `tag/` is optional and may be omitted.

If left unspecified, it will be set to a default value. | -* Items with `…`​ after them can be used multiple times including zero times.
- e.g. `[t/TAG]…​` can be used as ` ` (i.e. 0 times), `t/friend`, `t/friend t/family` etc. +For example, the command format for `add`: -* Parameters can be in any order.
- e.g. if the command specifies `n/NAME p/PHONE_NUMBER`, `p/PHONE_NUMBER n/NAME` is also acceptable. +``` +add c/CATEGORY_NAME n/ITEM_NAME p/PRICE [d/DATE] +``` -* If a parameter is expected only once in the command but you specified it multiple times, only the last occurrence of the parameter will be taken.
- e.g. if you specify `p/12341234 p/56785678`, only `p/56785678` will be taken. +- `add` is the `command` name. +- `c/`, `n/`, `p/`, `d/` are `tag/`s to denote fields of _category_, _name_, _price_ and _date_ respectively. +- `CATEGORY_NAME`, `ITEM_NAME`, `PRICE`, `DATE` are `PARAMETERS` to be supplied to the aforementioned `tag/`s. +- `[d/DATE]` indicates that the field for the date is optional. -* Extraneous parameters for commands that do not take in parameters (such as `help`, `list`, `exit` and `clear`) will be ignored.
+Don't worry if it takes a bit of time to get used to the commands. Once you're familiar with the commands, you'll be able to add expenses quickly and easily. + +
+ +**:information_source: Information about the command format**
+ +Before diving further into the guide, here are some things to take note about the way we formatted commands for FastTrack in this user guide. + +- Parameters can be in any order.
+ e.g. if the command specifies `c/CATEGORY_NAME p/PRICE`, `p/PRICE c/CATEGORY_NAME` is also acceptable. + +- If a parameter is expected only once in the command, but you specified it multiple times, only the **last occurrence** of the parameter will be taken.
+ e.g. if you specify `p/4.50 p/5.80`, only `p/5.80` will be taken. + +- Extraneous parameters for commands that do not take in parameters (such as `help`, `exit`) will be ignored.
e.g. if the command specifies `help 123`, it will be interpreted as `help`.
-### Viewing help : `help` +
-Shows a message explaning how to access the help page. +**:warning: Warning about Date Formatting:**
+Some commands may include the entry of a `DATE`, like the `add` command or the `addrec` command. A specific date format, `Day/Month/Year` should be used to format the date provided. -![help message](images/helpMessage.png) +However, if the day is **within 1-31** but **not a valid date**, _e.g. 30th February_, FastTrack will smartly correct your date provided to the last date of the month.
i.e, `30/2/2023` will be corrected to `28/2/2023`. -Format: `help` +
+ +

+ Back to Top +

+--- + +# **Category Features** + +FastTrack makes it easy for you to keep track of your spending by organizing expenses into categories. A category is like a folder that holds all your expenses that fall under a specific theme. For example, you might have a category called Groceries where you record all purchases from Fairprice or NTUC. -### Adding a person: `add` +To create a category in FastTrack, simply give it a name, such as Entertainment or Transportation. You can also add a short text summary to give yourself more context about the category. -Adds a person to the address book. +FastTrack even has a default Misc category for any expenses that you haven't categorized yet, however, this category is not modifiable or accessible. -Format: `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…​` +
+ +**:information_source: Info**
+Note that category names in FastTrack are case-insensitive. For example, a category named `Groceries` will be treated as the exact same category as `groceries`. -
:bulb: **Tip:** -A person can have any number of tags (including 0)
-Examples: -* `add n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01` -* `add n/Betsy Crowe t/friend e/betsycrowe@example.com a/Newgate Prison p/1234567 t/criminal` +### Command Summary + +| Feature | Command Format | Examples | +| -------------------------------------------------------- | ------------------------------------------- | ---------------------------------- | +| [**List Categories**](#listing-categories-lcat) | `lcat` | `lcat` | +| [**Add Category**](#adding-a-category-addcat) | `addcat c/CATEGORY_NAME s/SUMMARY` | `addcat c/Groceries s/for living` | +| [**Delete Category**](#deleting-a-category-delcat) | `delcat INDEX` | `delcat 1` | +| [**Edit Category**](#editing-a-category-edcat) | `edcat INDEX [c/CATEGORY_NAME] [s/SUMMARY]` | `edcat 1 c/New Name s/New Summary` | +| [**Category Summary**](#viewing-category-summary-sumcat) | `sumcat INDEX` | `sumcat 2` | -### Listing all persons : `list` +--- -Shows a list of all persons in the address book. +## **Listing Categories** `lcat` -Format: `list` +Displays the list of categories in FastTrack. -### Editing a person : `edit` +Format: `lcat` -Edits an existing person in the address book. +### Demonstration -Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]…​` +1. Type `lcat` into the command box +2. FastTrack displays the list of categories with the confirmation message `Listed all categories` -* Edits the person at the specified `INDEX`. The index refers to the index number shown in the displayed person list. The index **must be a positive integer** 1, 2, 3, …​ -* At least one of the optional fields must be provided. -* Existing values will be updated to the input values. -* When editing tags, the existing tags of the person will be removed i.e adding of tags is not cumulative. -* You can remove all the person’s tags by typing `t/` without - specifying any tags after it. +![FastTrack lcat](images/demo/category/lcat.png) -Examples: -* `edit 1 p/91234567 e/johndoe@example.com` Edits the phone number and email address of the 1st person to be `91234567` and `johndoe@example.com` respectively. -* `edit 2 n/Betsy Crower t/` Edits the name of the 2nd person to be `Betsy Crower` and clears all existing tags. +## **Adding a category** `addcat` -### Locating persons by name: `find` +Adds a new category to FastTrack. If a category with the same name already exists, this command will not execute. -Finds persons whose names contain any of the given keywords. +Format: `addcat c/CATEGORY_NAME s/SUMMARY` -Format: `find KEYWORD [MORE_KEYWORDS]` +| Parameter | Description | +| --------------- | --------------------------------------------------- | +| `CATEGORY_NAME` | Title of the category to be added. | +| `SUMMARY` | Short summary of what this category keeps track of. | + +### Examples + +- `addcat c/Groceries s/for living` creates a new `Groceries` category with the summary of `for living`. +- `addcat c/Entertainment s/for fun!` creates a new `Entertainment` category with the summary of `for fun!`. + +### Demonstration + +1. Enter the command `lcat` to switch to the **Category Display** +2. Enter the command `addcat c/Groceries s/for living` into the command box +3. FastTrack adds the new category to the category list with the confirmation message `New category added: groceries` + +![FastTrack addcat](images/demo/category/addcat.png) + +## **Deleting a category** `delcat` + +Deletes the category at the specified `INDEX` in the category list. + +Format: `delcat INDEX` + +| Parameter | Description | +| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `INDEX` | The index number shown in the displayed category list.

It must be a positive integer i.e. 1, 2, 3, ...

Expenses previously categorised under the specified category will be automatically re-categorized under the `Misc` category.
| + +
+**:information_source: Info:**
+ +If you delete a category that has existing expenses associated with it, those expenses will be automatically reassigned to the default internal `Misc` category. But don't worry! You can still re-assign them later with the [edit expense command](#editing-an-expense-edexp). To avoid losing track of expenses, we recommend that you review and update your categories periodically, rather than deleting them altogether. + +
+ +### Examples -* The search is case-insensitive. e.g `hans` will match `Hans` -* The order of the keywords does not matter. e.g. `Hans Bo` will match `Bo Hans` -* Only the name is searched. -* Only full words will be matched e.g. `Han` will not match `Hans` -* Persons matching at least one keyword will be returned (i.e. `OR` search). - e.g. `Hans Bo` will return `Hans Gruber`, `Bo Yang` +- `lcat` followed by `delcat 2` deletes the second category in the category list +- `lcat` followed by `delcat 1` deletes the first category in the category list -Examples: -* `find John` returns `john` and `John Doe` -* `find alex david` returns `Alex Yeoh`, `David Li`
- ![result for 'find alex david'](images/findAlexDavidResult.png) +### Demonstration -### Deleting a person : `delete` +1. Enter the command `lcat` to switch to the **Category Display** +2. Enter the command `delcat 7` into the command box +3. FastTrack deletes the seventh category `Food` from the category list with the confirmation message `Deleted category: food` -Deletes the specified person from the address book. +![FastTrack delcat](images/demo/category/delcat.png) + +## **Editing a category** `edcat` + +Edits the category at the specified `INDEX` in the category list. + +Format: `edcat INDEX [c/CATEGORY_NAME] [s/SUMMARY]` + +Both `CATEGORY_NAME` and `SUMMARY` are optional by themselves, but **at least** one of them must be specified in addition +to `INDEX`, otherwise the command will not be executed. + +| Parameter | Description | +| --------------- | --------------------------------------------------------------------------------------------------------- | +| `INDEX` | The index of the category to be edited.

It must be a positive integer i.e. 1, 2, 3, ... | +| `CATEGORY_NAME` | The new name of the category being edited at the specified index.

This parameter is optional. | +| `SUMMARY` | The new summary of the category being edited at the specified index.

This parameter is optional. | + +### Examples + +- `edcat 1 c/Drink` changes the name of the first category in the category list to `Drink` +- `edcat 2 c/Food s/Eating out` changes the name and summary of the second category in the category list to `Food` and `Eating out` respectively. + +### Demonstration + +1. Enter the command `lcat` to switch to the **Category Display** +2. Enter the command `edcat 1 c/Drink` into the command box +3. FastTrack edits the name of the first category `Food` from the category list to `Drink` with the confirmation message `Edited category: Drink` + +![FastTrack edcat1](images/demo/category/edcat1.png) +![FastTrack edcat2](images/demo/category/edcat2.png) + +## **Viewing Category Summary** `sumcat` + +Displays the category summary for a category. + +Format: `sumcat INDEX` + +| Parameter | Description | +| --------- | ------------------------------------------------------------------------------------------------ | +| `INDEX` | The index of the category to be edited.

It must be a positive integer i.e. 1, 2, 3, ... | + +### Demonstration + +1. Enter the command `lcat` to switch to the **Category Display** +2. Enter the command `sumcat 2` into the command box. +3. FastTrack displays the summary of the Entertainment category in the Results Display. + +![FastTrack sumcat](images/demo/category/sumcat.png) + +

+ Back to Top +

+ +--- + +# **Expense Features** + +An **expense** is a single purchase that you want to track. Each expense has a _name_, _price_, _category_, and _date_. With FastTrack, you can easily duplicate an expense if you happen to make the same purchase multiple times, such as buying a coffee from CoolSpot every morning on your way to NUS. + +Finally, there are **recurring expenses**. These are expenses that are charged on a regular basis, such as a monthly subscription to Netflix or an annual Heroku subscription. Instead of manually creating an expense every time the payment is due, you can set up a recurring expense in FastTrack. +Simply specify the start date, interval (daily, weekly, monthly, yearly), and end date (if applicable), and FastTrack will automatically generate the expenses for you. + +### Command Summary + +#### One-time Expenses + +| Feature | Command Format | Examples | +| ---------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------- | +| [**List Expenses**](#listing-expenses-list) | `list [c/CATEGORY_NAME] [t/TIMEFRAME]` | `list c/Food t/month` | +| [**Add Expense**](#adding-an-expense-add) | `add c/CATEGORY_NAME n/ITEM_NAME p/PRICE [d/DATE]` | `add c/Food p/20 n/Mac d/14/2/23` | +| [**Delete Expense**](#deleting-an-expense-delete) | `delete INDEX` | `delete 1` | +| [**Edit Expense**](#editing-an-expense-edexp) | `edexp INDEX [c/CATEGORY_NAME] [n/EXPENSE_NAME] [d/DATE] [p/PRICE]` | `edexp 1 c/Food n/Mac d/20/4/23 p/10` | +| [**Find Expense**](#search-for-an-expense-by-keyword-find) | `find KEYWORD [MORE_KEYWORDS]` | `find KFC chicken` | + +#### Recurring Expenses + +| Feature | Command Format | Example | +| -------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | -------------------------------------------------------------- | +| [**List Recurring Expense**](#listing-recurring-expenses-lrec) | `lrec` | `lrec` | +| [**Add Recurring Expense**](#adding-a-recurring-expense-addrec) | `addrec c/CATEGORY_NAME n/ITEM_NAME p/PRICE t/INTERVAL sd/START_DATE [ed/END_DATE]` | `addrec c/Shows n/Netflix p/10 t/month sd/10/3/23 ed/10/03/24` | +| [**Delete Recurring Expense**](#deleting-a-recurring-expense-delrec) | `delrec INDEX` | `delrec 1` | +| [**Edit Recurring Expense**](#editing-a-recurring-expense-edrec) | `edrec INDEX [c/CATEGORY_NAME] [n/EXPENSE_NAME] [p/PRICE] [t/INTERVAL] [ed/END_DATE]` | `edrec 1 c/Show n/Disney Plus p/2 t/week ed/10/5/24` | + +--- + +## **One-Time Expenses** + +--- + +## **Listing expenses** `list` + +The `list` feature in FastTrack allows you to view all your expenses. You can filter the list based on specific categories and timeframes to get a more customized view of your spending. + +If you apply the `CATEGORY_NAME` filter, only expenses associated with that particular category will be displayed. For instance, if you filter by `groceries`, you'll only see the expenses you've categorized as `groceries`. + +If you apply the `TIMEFRAME` filter, you can see expenses that fall within a particular time period. For example, you could filter by `month` to see only the expenses you incurred in the current month. + +If you don't specify any filters, the expense list will show all your expenses by default. + +Format: `list [c/CATEGORY_NAME] [t/TIMEFRAME] [r/RECUR_PERIOD]` + +| Parameter | Description | +| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `CATEGORY_NAME` | The name of the category of which expenses are classed under.

This parameter is optional. | +| `TIMEFRAME` | The timeframe of which expenses were added.

The timeframes available are:
1. `week` (alias: `w`)
2. `month` (alias: `m`)
3. `year` (alias: `y`)

This parameter is optional. | + +
+**:information_source: What is a timeframe?**
+ +A `TIMEFRAME` allows you to set a specific interval to filter your expenses. `t/w` or `t/week` is a timeframe representing the current week, while `t/m` or `t/month` is a timeframe representing the current month, and `t/y` or `t/year` is a timeframe representing the current year. + +
+ +### Examples + +- `list` +- `list c/Groceries t/week` +- `list c/Entertainment t/month` +- `list c/Food` +- `list t/w` +- `list c/Entertainment t/year` + +### Demonstration + +### **List Expenses by Category** + +1. Enter the command `list c/drink` into the command box +2. FastTrack displays the expenses under the category `Drink` with the confirmation message `2 expenses listed`. The number of expenses may differ for every user. + +![FastTrack list1](images/demo/expense/list1.png) + +### **List All Expenses** + +1. Enter the command `list` into the command box +2. FastTrack displays all expenses with the confirmation message `5 expenses listed`. The number of expenses may differ for every user. + +![FastTrack list2](images/demo/expense/list2.png) + +### **List Expenses by Timeframe** + +1. Enter the command `list t/w` into the command box +2. FastTrack displays all expenses within the current week with the confirmation message `2 expenses listed`. The number of expenses may differ for every user. + +![FastTrack list3](images/demo/expense/list3.png) + +
+ +**:information_source: Using both `CATEGORY_NAME` and `TIMEFRAME` filters:**
+ +- Using both the category and timeframe filters will only display expenses that satisfy both the filter conditions.
+e.g. in `list c/food t/week`, only expenses with both the category name "Food" and date falling within the current week will be displayed. +
+ +## **Adding an expense** `add` + +Adds a new one-time expense to FastTrack. + +Format: `add c/CATEGORY_NAME n/ITEM_NAME p/PRICE [d/DATE]` + +| Parameter | Description | +| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `CATEGORY_NAME` | The category which the expense should be classified under.

If there is no such category in FastTrack, a new category will be created with the specified category name. | +| `ITEM_NAME` | Name of the expense being added. | +| `PRICE` | The price of the expense being added.

The specified price should be a number, e.g. 4, 4.50. | +| `DATE` | The date of the expense being added.

The date format should be d/m/yyyy.

This is an optional input, and if left unspecified, the date of the expense will be set to the **current date** by default. | + +### Examples + +- `add c/groceries n/milk p/4.50 ` +- `add c/entertainment p/20 n/movie night d/14/2/23` + +### Demonstration + +1. Enter the command `add c/groceries n/milk p/4.50` into the command box +2. FastTrack adds the new expense under the new category `Groceries` with the confirmation message `New expense added: Name: milk, Amount: $4.5, Date: 2023-04-07, Category: groceries`. + +![FastTrack add1](images/demo/expense/add1.png) + +3. Enter the command `lcat` to switch to the **Category Display**. Notice how FastTrack has automatically created a new category `Groceries` in the category list! + +![FastTrack add2](images/demo/expense/add2.png) + +## **Deleting an expense** `delete` + +Deletes an expense at the specified `INDEX` in the expense list. Format: `delete INDEX` -* Deletes the person at the specified `INDEX`. -* The index refers to the index number shown in the displayed person list. -* The index **must be a positive integer** 1, 2, 3, …​ +| Parameter | Description | +| --------- | ----------------------------------------------------------------------------------------------------------------- | +| `INDEX` | The index number shown in the displayed categories list.

It must be a positive integer i.e. 1, 2, 3, ... | + +### Examples + +- `list` followed by `delete 2` deletes the second expense in the expense list +- `find movie` followed by `delete 1` deletes the first expense in the results of the `find` command + +### Demonstration + +1. Type the command `list` to switch to the **Expense Display** +2. Enter the command `delete 2` into the command box +3. FastTrack deletes the second expense in the expense list with the confirmation message `Deleted expense: Name: milk, Amount: $4.5, Date: 2023-04-07, Category: groceries`. + + + +![FastTrack delete](images/demo/expense/delete.png) + + +## **Editing an expense** `edexp` + +Edits the expense at the specified `INDEX` in the expense list. + +Format: `edexp INDEX [c/CATEGORY_NAME] [n/EXPENSE_NAME] [d/DATE] [p/PRICE] [r/RECUR_PERIOD]` + +Every parameter except for `INDEX` is optional by themselves, but **at least** one of them must be specified in addition +to `INDEX`, otherwise the command will not be executed. + +| Parameter | Description | +| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | +| `INDEX` | The index of the expense to be edited.

It must be a positive integer i.e. 1, 2, 3, ... | +| `CATEGORY_NAME` | The new category name of the expense to be changed to.

This parameter is optional. | +| `EXPENSE_NAME` | The new expense name of the expense to be changed to.

This parameter is optional. | +| `DATE` | The new date of the expense to be changed to.

The date format should be d/m/yyyy.

This parameter is optional. | +| `PRICE` | The new price of the expense to be changed to.

The specified price should be a number, e.g. 4, 4.50.

This parameter is optional. | + +### Examples + +- `edexp 1 c/groceries` changes the category of the first expense in the expense tracker +- `edexp 2 p/20 n/movie night` changes the price and name of the second expense in the expense tracker + +### Demonstration + +1. Type the command `list` to switch to the **Expense Display** +2. Enter the command `edexp 2 p/20 n/movie night c/entertainment` +3. FastTrack edits the second expense in the expense list with the confirmation message `Edited expense: Name: movie night, Amount: $20.0, Date: 2023-04-03, Category: entertaiment`. -Examples: -* `list` followed by `delete 2` deletes the 2nd person in the address book. -* `find Betsy` followed by `delete 1` deletes the 1st person in the results of the `find` command. +![FastTrack edexp1](images/demo/expense/edexp1.png) +![FastTrack edexp2](images/demo/expense/edexp2.png) -### Clearing all entries : `clear` +## **Search for an expense by keyword** `find` -Clears all entries from the address book. +Find expenses whose names contain any of the given keywords. -Format: `clear` +Format: `find KEYWORD [MORE_KEYWORDS]` + +- The search is case-insensitive. e.g. `dinner` will match `Dinner` +- The order of the keywords does not matter. e.g. `ramen Dinner` will match `Dinner ramen` +- Only the name of the expense is searched +- Only full words will be matched e.g. `dinn` will not match `dinner` +- Expenses matching at least one keyword will be returned + e.g. `movie dinner` will return `dinner with Alex`, `movie with friends` + +### Examples + +Suppose you have 3 expenses logged: + +``` +Date: 2023-03-02, Category: Food, Name: McDonald's, Price: $7.50 +Date: 2023-03-02, Category: Food, Name: KFC, Price: $6.00 +Date: 2023-03-03, Category: Groceries, Name: Milk, Price: $4.00 +``` + +- `find kfc milk` returns `Milk` and `KFC` +- `find mcdonald's` returns `McDonald's`
+ +### Demonstration + +1. Enter the command `find movie` into the command box to find expenses with the keyword `movie` +2. FastTrack filters the expense list to show only the expenses matching the given keyword, with the confirmation message `Edited expense: Name: movie night, Amount: $20.0, Date: 2023-04-03, Category: entertaiment`. + +![FastTrack find](images/demo/expense/find.png) + +

+ Back to Top +

+ +--- + +## **Recurring Expenses** + +--- + +## **Listing Recurring Expenses** `lrec` + +Displays the list of recurring expenses in FastTrack. + +Format: `lrec` + +### Demonstration + +1. Type `lrec` into the command box +2. FastTrack displays the list of recurring expenses with the confirmation message `Listed all recurring expenses` + +![FastTrack lrec](images/demo/recurring_expense/lrec.png) + +## **Adding a Recurring Expense** `addrec` + +Adds a recurring expense to FastTrack. + +Format: `addrec c/CATEGORY_NAME n/ITEM_NAME p/PRICE t/INTERVAL sd/START_DATE [ed/END_DATE]` + +| Parameter | Description | +| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `CATEGORY_NAME` | The category which the recurring expense should be classified under.

If there is no such category, a new category will be created with the specified category name. | +| `ITEM_NAME` | Name of the recurring expense being added. | +| `PRICE` | The price of the recurring expense being added.

The specified price should be a number, e.g. 4, 4.50. | +| `INTERVAL` | The period with which the expense is recurring.

The timeframes available are:
1. `day`
2. `week`
3. `month`
4. `year` | +| `START_DATE` | The starting date of the recurring expense.

The date format should be d/m/yyyy. | +| `END_DATE` | The ending date of the recurring expense.

The date format should be d/m/yyyy.

This parameter is optional. | + +
+ +**:information_source: Info**
+ +Note that once a recurring expense is added, it automatically adds a series of expenses to the expense list at the specified interval until the `END_DATE`. If an `END_DATE` is not yet specified, the expenses will be added up to the current date. + +
+ +
:bulb: Tip: + +FastTrack's recurring expense feature is designed to help you keep track of regular expenses that occur at a fixed interval, such as monthly subscription fees or cloud storage bills. +By setting up a recurring expense, you save precious time and effort by automating the process of adding these expenses to FastTrack. + +
+ +
+ +**:exclamation: Caution**
+ +Avoid setting an `END_DATE` that is too far in the future or a `START_DATE` that is too far in the past. Setting a date range that spans a large number of years or generates a large number of expenses may cause FastTrack to become temporarily unresponsive. +For example, if the current date is `3/2/2023` and the `START_DATE` is set to `3/2/2000` with an `INTERVAL` of `day`, this will generate over 8,000 expenses and may cause performance issues. + +
+ +### Examples + +- `addrec n/milk c/groceries p/4.50 sd/20/3/2023 t/month` +- `addrec n/milk c/groceries p/4.50 sd/20/3/2023 ed/15/5/2023 t/w` + +### Demonstration + +1. Enter the command `addrec n/milk c/groceries p/4.50 sd/20/1/2023 t/week` to create a weekly recurring expense starting on 20/1/2023. +2. FastTrack creates the new recurring expense with the confirmation message `New recurring expense added: Recurring Expense: milk, Amount: 4.5, Category: groceries, Start Date: 2023-01-20, End Date: Ongoing, Recurring Expense Type: WEEKLY` + +![FastTrack addrec1](images/demo/recurring_expense/addrec1.png) + +3. Enter the command `list` to switch to the **Expense Display**. Notice that FastTrack has automatically added the weekly expenses in the expense list! + +![FastTrack addrec2](images/demo/recurring_expense/addrec2.png) + +## **Deleting a recurring expense** `delrec` + +Deletes an expense category at the specified `INDEX` in the recurring expense list. + +Format: `delrec INDEX` + +| Parameter | Description | +| --------- | ------------------------------------------------------------------------------------------------------------------------ | +| `INDEX` | The index number shown in the displayed recurring expense list.

It must be a positive integer i.e. 1, 2, 3, ... | + +
:information_source: Automatic Deletion of Recurring Expenses: + +If a recurring expense's end date has already passed, FastTrack automatically deletes the recurring expense the next time the application is started. This means you do not need to worry about manually deleting recurring expenses which are no longer applicable! + +
+ +### Examples + +- `lrec` followed by `delrec 2` deletes the second recurring expense in the recurring expense list +- `lrec` followed by `delrec 1` deletes the first recurring expense in the recurring expense list + +### Demonstration + +1. Enter the command `lrec` to switch to the **Recurring Expense Display** +2. Enter the command `delrec 2` +3. FastTrack deletes the second recurring expense in the recurring expense list with the confirmation message `Deleted recurring expense: Recurring Expense: Netflix, Amount: 16, Category: entertainment, Start Date: 2023-01-01, End Date: Ongoing, Recurring Expense Type: MONTHLY`. + +![FastTrack delrec](images/demo/recurring_expense/delrec.png) + +## **Editing a recurring expense** `edrec` + +Edits the expense at the specified `INDEX` + +Format: `edrec INDEX [c/CATEGORY_NAME] [n/EXPENSE_NAME] [p/PRICE] [t/INTERVAL] [ed/END_DATE]` + +Every parameter except for `INDEX` is optional by themselves, but **at least** one of them must be specified in addition +to `INDEX`, otherwise the command will not go through. + +| Parameter | Description | +| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `INDEX` | The index of the recurring expense to be edited.

It must be a positive integer i.e. 1, 2, 3, ... | +| `CATEGORY_NAME` | The new category name of the recurring expense to be changed to.

This parameter is optional. | +| `EXPENSE_NAME` | The new expense name of the recurring expense to be changed to.

This parameter is optional. | +| `PRICE` | The new price of the recurring expense to be changed to.

The specified price should be a number, e.g. 4, 4.50.

This parameter is optional. | +| `INTERVAL` | The new recurrence period of the expense to be changed.
The timeframes available are:
1. `day`
2. `week`
3. `month`
4. `year`

This parameter is optional. | +| `END_DATE` | The new ending date of recurring expense.

The date format should be d/m/yyyy.

This parameter is optional. | + +
+ +**:exclamation: Caution**
+ +If you want to stop a recurring expense before its intended `END_DATE`, make sure to delete it before the current date. +If you edit the recurring expense to end before the current date, this only prevents new expenses from being added, but expenses that were previously generated will still exist in FastTrack. + +
+ +### Examples + +- `edrec 1 c/groceries t/week` updates the category and recurrence period first recurring expense in the expense tracker. +- `edrec 2 p/4.50 ed/15/5/2023` updates the price and ending date of the second recurring expense in the expense tracker. + +### Demonstration + +1. Enter the command `lrec` to switch to the **Recurring Expense Display**. +2. Say you have upgraded to a Netflix yearly subscription plan - enter the command `edrec 2 p/200 t/year` . +3. FastTrack edits the second recurring expense in the recurring expense list with the confirmation message `Edited recurring expense generator: Recurring Expense: Netflix, Amount: 200.0, Category: entertainment, Start Date: 2023-01-20, End Date: Ongoing, Recurring Expense Type: YEARLY`. + +![FastTrack edrec1](images/demo/recurring_expense/edrec1.png) +![FastTrack edrec2](images/demo/recurring_expense/edrec2.png) + +

+ Back to Top +

+ +--- + +# General Features + +### Command Summary + +| Feature | Command Format | Examples | +| --------------------------------------------- | -------------- | ------------ | +| [**Set Budget**](#setting-a-budget-set) | `set p/AMOUNT` | `set p/1000` | +| [**Help**](#viewing-help-help) | `help` | `help` | +| [**Exit program**](#exiting-fasttrack-exit) | `exit` | `exit` | +| [**Clear data**](#clearing-all-entries-clear) | `CLEAR` | `CLEAR` | + +--- + +## **Setting A Budget** `set` + + +Sets a monthly budget for FastTrack. For first-time users of FastTrack, no budget is set and expense statistics are not updated. + + +In order to view all the expense statistics, you must first set a budget using this command. + +FastTrack derives the weekly budget from this monthly budget by dividing the monthly budget by 4. + +Format `set p/AMOUNT` + +| Parameter | Description | +| --------- | ---------------------------------------------------------------------------------------- | +| `AMOUNT` | The monthly budget amount to set. The specified budget should be a number, e.g. 4, 4.50. | + +
-### Exiting the program : `exit` +**:exclamation: Caution**
-Exits the program. +FastTrack does not allow setting a budget of $0. + +
+ +### Examples + +* `set p/500` sets the monthly budget of FastTrack to $500. + +### Demonstration +1. Enter the command `set p/500` to set the monthly budget of FastTrack to $500. +2. FastTrack updates the monthly budget to $500 with the confirmation message `Monthly budget successfully set to $500.0` + +![FastTrack set](images/demo/general/set.png) + +## **Category Autocompletion** + +FastTrack offers a powerful and time-saving feature that autocompletes your category names for you! When you start typing `c/`, FastTrack provides a list of suggested category names as a popup above the command box. +Give it a try and see how much time you can save with this feature! + +### How to Use Category Autocompletion? + +- Type `c/` in the command box to trigger the autocompletion feature. +- FastTrack will display a list of suggested category names above the command box. +- To select a category name from the list, use the `UP` and `DOWN` arrow keys to navigate through the suggestions list. +- Press `ENTER` to autocomplete the selected category name. +- If the category you're looking for is conveniently at the bottom of the list, simply press `TAB` to autocomplete the first suggested category without having to navigate through the list manually! +- If you decide not to use any of the suggested categories, just continue typing your own category name as per normal. + +
:bulb: Tip: + +Want to use `TAB` directly without navigating inside the suggestion list? +Narrow down the list of suggested categories by typing the first few words of your desired category name. Once the option appears at the bottom of the list, simply press `TAB` for autocompletion. + +
+ +
+**:exclamation: Caution**
+ + +To use category autocompletion, make sure that `c/` is the last text you've entered into the command box. +If there is any other text in front of `c/`, the autocompletion feature will be disabled. + + +
+ +### Demonstration + + +1. Enter `list c/` into the command box. +2. A list of suggested categories appear in a popup above the command box. +3. Navigate into the suggestion list using the `UP` arrow key and press `ENTER` on the desired category `Transportation`. +4. This autocompletes the category name. +5. If you need to navigate out of the suggestion list, press the `DOWN` arrow key until the cursor returns to the command box. + + +![FastTrack autocomplete_a1](images/demo/general/autocomplete_a1.png) +![FastTrack autocomplete_a2](images/demo/general/autocomplete_a2.png) + +1. Enter `list c/` into the command box. +2. A list of suggested categories appear in a popup above the command box. +3. If the desired category `Shopping` is the first suggestion in the list (the bottom-most suggestion), press `TAB` within the command box. +4. This autocompletes the category name. + +![FastTrack autocomplete_b1](images/demo/general/autocomplete_b1.png) +![FastTrack autocomplete_b2](images/demo/general/autocomplete_b2.png) + +## **Clearing all entries** `CLEAR` + +Clears all entries from FastTrack. This command removes all stored expenses, recurring expenses and categories. + +Format: `CLEAR` + +
+**:exclamation: Caution**
+ +This command will delete **all** the data stored in FastTrack apart from the stored monthly budget. To minimise the risk of accidentally using this command, we have made it such that the command only works when the word `CLEAR` is fully uppercase. + +Exercise caution before using this command. + +
+ +### Demonstration + +1. Enter `CLEAR` in the command box. +2. FastTrack clears all previously logged expenses, recurring expenses and categories, with the confirmation message `Deleted all prior entries`. + +![FastTrack clear](images/demo/general/clear.png) + +## **Exiting FastTrack** `exit` + +After logging your expenses, you might want to close the application and ensure your data is saved. +This command closes FastTrack and saves the data to the `fastTrack.json` file located on computer's hard disk. Format: `exit` -### Saving the data +## **Viewing help** `help` + +Shows a message explaining how to access the help page, as well as a quick rundown of what commands can be used. + +Format: `help` + +![help message](images/helpMessage.png) + +

+ Back to Top +

-AddressBook data are saved in the hard disk automatically after any command that changes the data. There is no need to save manually. +# Expense Statistics Feature -### Editing the data file +FastTrack provides you with real-time statistics on your spending to help you keep track of your monthly budget. +Here are the types of statistics displayed and what they mean. -AddressBook data are saved as a JSON file `[JAR file location]/data/addressbook.json`. Advanced users are welcome to update data directly by editing that data file. +![FastTrack expense_statistic](images/demo/general/summary.png) + +## Monthly spending statistic + +This statistic represents the **total amount of money you have spent** in the **current month**. +It includes all expenses recorded in the current month. + + +For example, if the current month is March, this statistic shows the total amount of money spent in March. + +## Monthly remaining statistic + +This statistic represents the **amount of money you have left** from your **monthly** budget. +It gives you an idea of how much money you have left to spend for the rest of the month. + +## Monthly percentage change statistic + + +This statistic represents the percentage **increase** or **decrease** in your **monthly** spending relative to the previous month. +The indicator colour is **red** if it is a percentage increase and **green** if it is a percentage decrease. + + +For example, if you spent $500 last month and $750 this month, the monthly percentage change indicator would be `+50.00%` and be displayed in a red colour. +If you spent $750 last month and $500 this month, the monthly percentage change would be `-33.30%` and be displayed in a green color. + +## Weekly spending statistic + + +This statistic represents the **total amount of money you have spent** in the **current week**, starting from Monday to Sunday. +This gives you an idea of how much money you are spending on a weekly basis. + +## Weekly remaining statistic + +This statistic represents the **amount of money you have left** from your **weekly** budget. +Your weekly budget is the value of your monthly budget divided by four. This gives you an idea of how much money you have left to spend for the rest of the week.
:exclamation: **Caution:** -If your changes to the data file makes its format invalid, AddressBook will discard all data and start with an empty data file at the next run. + + +Please take note that this value should be treated as a rough guide.
+Even if you have exceeded your previous week's budget, this statistic will show that you have more remaining, as the weekly budget is fixed based on the monthly budget. + +Therefore, it is important to use this value as an estimate and not solely rely on it for your spending decisions! + +
+ +## Weekly percentage change statistic + +This statistic represents the percentage **increase** or **decrease** in your **weekly** spending relative to the previous week. +The indicator colour is **red** if it is a percentage increase and **green** if it is a percentage decrease. + +For example, if you spent $500 last week and $750 this week, the weekly percentage change indicator would be `+50.00%` and be displayed in a red colour. +If you spent $750 last week and $500 this week, the weekly percentage change would be `-33.30%`. + +## Total spent statistic + +This statistic represents the **total amount of money you have spent to date**, starting from the first expense you recorded in FastTrack. + +This gives you an idea of how much money you have spent over the period of time from when you started tracking your expenses. + +## Budget utilisation percentage statistic + +This statistic represents the percentage of your monthly budget that you have already utilised in the current month. + +For example, if your monthly budget is $1000, and you have already spent $500, your budget utilised percentage would be `50%`. +This gives you an idea of how much of your monthly budget you have used up. + +
+ +**:exclamation: Caution**
+ +Even if you have exceeded your budget, this statistic will reflect that you have fully utilised your budget, and will remain at `100%`. +
-### Archiving data files `[coming in v2.0]` +

+ Back to Top +

-_Details coming soon ..._ +--- --------------------------------------------------------------------------------------------------------------------- +# Saving the data -## FAQ +All data in FastTrack are saved in the hard disk automatically after any command that changes the data. There is no need to save manually. + +# Editing the data file + +FastTrack's data are saved as a JSON file `[JAR file location]/data/fastTrack.json`. Advanced users who are familiar with JSON (JavaScript Object Notation) are welcome to update data directly by editing that data file. + +
:exclamation: **Caution:** +If your changes to the data file makes its format invalid, FastTrack will discard all data and start with an empty data file at the next run. +
+ +--- + +# Frequently Asked Questions **Q**: How do I transfer my data to another Computer?
-**A**: Install the app in the other computer and overwrite the empty data file it creates with the file that contains the data of your previous AddressBook home folder. +**A**: Install the app in the other computer and overwrite the empty data file it creates with the file that contains the data of your previous FastTrack home folder. + +**Q**: Does FastTrack require a Wi-Fi network?
+**A**: No, FastTrack does not need any sort of internet connection to run! You can be rest assured that your data is kept safe locally on your computer. However, accessing the user guide and developer guide which are hosted online will require an internet connection. + +**Q**: Why are some of my expenses being categorised as `Misc`?
+**A**: `Misc` is an internal default category in FastTrack that represents an unclassified expense. If you see an expense with the `Misc` category, chances are, the category it was previously associated with was deleted. + +**Q**: My expense name gets cut off with trailing ellipses `...`, how do I fix this?
+**A**: Try resizing the FastTrack window size by increasing its width until the full expense names are within view. --------------------------------------------------------------------------------------------------------------------- +**Q**: Can I set reminders for recurring expenses in FastTrack?
+**A**: No, FastTrack does not currently have a built-in feature for setting reminders for recurring expenses. +However, you can use an external calendar or reminder app to keep track of recurring expenses. -## Command summary +**Q**: Does FastTrack integrate with payment systems like credit cards or PayPal?
+**A**: Currently, FastTrack does not support integration with external payment systems. However, we are constantly improving and expanding our features, and we plan to explore integrating with popular payment systems in the future. Stay tuned for more updates! -Action | Format, Examples ---------|------------------ -**Add** | `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…​`
e.g., `add n/James Ho p/22224444 e/jamesho@example.com a/123, Clementi Rd, 1234665 t/friend t/colleague` -**Clear** | `clear` -**Delete** | `delete INDEX`
e.g., `delete 3` -**Edit** | `edit INDEX [n/NAME] [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [t/TAG]…​`
e.g.,`edit 2 n/James Lee e/jameslee@example.com` -**Find** | `find KEYWORD [MORE_KEYWORDS]`
e.g., `find James Jake` -**List** | `list` -**Help** | `help` +

+ Back to Top +

diff --git a/docs/_config.yml b/docs/_config.yml index 6bd245d8f4e..7bdc137812d 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -1,4 +1,4 @@ -title: "AB-3" +title: "FastTrack" theme: minima header_pages: @@ -8,7 +8,7 @@ header_pages: markdown: kramdown -repository: "se-edu/addressbook-level3" +repository: "AY2223S2-CS2103T-W09-2/tp" github_icon: "images/github-icon.png" plugins: diff --git a/docs/_sass/minima/_base.scss b/docs/_sass/minima/_base.scss index 0d3f6e80ced..47f0b52f2ba 100644 --- a/docs/_sass/minima/_base.scss +++ b/docs/_sass/minima/_base.scss @@ -288,7 +288,7 @@ table { text-align: center; } .site-header:before { - content: "AB-3"; + content: "FastTrack"; font-size: 32px; } } diff --git a/docs/_sass/minima/skins/classic.scss b/docs/_sass/minima/skins/classic.scss index 37ea9c5244c..dad5c7cd3a7 100644 --- a/docs/_sass/minima/skins/classic.scss +++ b/docs/_sass/minima/skins/classic.scss @@ -6,7 +6,7 @@ $brand-color-dark: darken($brand-color, 25%) !default; $text-color: #111 !default; $background-color: #fdfdfd !default; -$code-background-color: #eef !default; +$code-background-color: #eff !default; $link-base-color: #2a7ae2 !default; $link-visited-color: darken($link-base-color, 15%) !default; diff --git a/docs/diagrams/ArchitectureDiagram.puml b/docs/diagrams/ArchitectureDiagram.puml index 4c5cf58212e..78503252488 100644 --- a/docs/diagrams/ArchitectureDiagram.puml +++ b/docs/diagrams/ArchitectureDiagram.puml @@ -9,6 +9,7 @@ Package " "<>{ Class Logic LOGIC_COLOR Class Storage STORAGE_COLOR Class Model MODEL_COLOR + Class AnalyticModel MODEL_COLOR Class Main #grey Class Commons LOGIC_COLOR_T2 } @@ -19,6 +20,7 @@ Class "<$documents>" as File UI_COLOR_T1 UI -[#green]> Logic UI -right[#green]-> Model +UI -right[#green]-> AnalyticModel Logic -[#blue]-> Storage Logic -down[#blue]-> Model Main -[#grey]-> UI @@ -28,6 +30,7 @@ Main -up[#grey]-> Model Main -down[hidden]-> Commons Storage -up[STORAGE_COLOR].> Model +Storage -up[STORAGE_COLOR].-> AnalyticModel Storage .right[STORAGE_COLOR].>File User ..> UI @enduml diff --git a/docs/diagrams/ArchitectureSequenceDiagram.puml b/docs/diagrams/ArchitectureSequenceDiagram.puml index ef81d18c337..b4027a54b59 100644 --- a/docs/diagrams/ArchitectureSequenceDiagram.puml +++ b/docs/diagrams/ArchitectureSequenceDiagram.puml @@ -4,7 +4,8 @@ Actor User as user USER_COLOR Participant ":UI" as ui UI_COLOR Participant ":Logic" as logic LOGIC_COLOR -Participant ":Model" as model MODEL_COLOR +Participant ":Model" as dataModel MODEL_COLOR +Participant ":AnalyticModel" as analytic MODEL_COLOR Participant ":Storage" as storage STORAGE_COLOR user -[USER_COLOR]> ui : "delete 1" @@ -13,13 +14,22 @@ activate ui UI_COLOR ui -[UI_COLOR]> logic : execute("delete 1") activate logic LOGIC_COLOR -logic -[LOGIC_COLOR]> model : deletePerson(p) -activate model MODEL_COLOR +logic -[LOGIC_COLOR]> dataModel : deleteExpense(e) +activate dataModel MODEL_COLOR -model -[MODEL_COLOR]-> logic -deactivate model +dataModel -[MODEL_COLOR]> analytic : updateAllStatistics() +activate analytic MODEL_COLOR +ref over analytic + Update statistics in UI +end ref +return +deactivate analytic -logic -[LOGIC_COLOR]> storage : saveAddressBook(addressBook) + +dataModel -[MODEL_COLOR]-> logic +deactivate dataModel + +logic -[LOGIC_COLOR]> storage : saveExpenseTracker(expenseTracker) activate storage STORAGE_COLOR storage -[STORAGE_COLOR]> storage : Save to file diff --git a/docs/diagrams/ComponentManagers.puml b/docs/diagrams/ComponentManagers.puml index 5e907dc1115..12080856630 100644 --- a/docs/diagrams/ComponentManagers.puml +++ b/docs/diagrams/ComponentManagers.puml @@ -5,27 +5,27 @@ skinparam arrowColor LOGIC_COLOR_T4 skinparam classBackgroundColor LOGIC_COLOR package Logic { -Class "<>\nLogic" as Logic +Class "<>\nLogic" as dataLogic Class LogicManager } package Model{ -Class "<>\nModel" as Model +Class "<>\nModel" as dataModel Class ModelManager } package Storage{ -Class "<>\nStorage" as Storage +Class "<>\nStorage" as dataStorage Class StorageManager } Class HiddenOutside #FFFFFF HiddenOutside ..> Logic -LogicManager .up.|> Logic -ModelManager .up.|> Model -StorageManager .up.|> Storage +LogicManager .up.|> dataLogic +ModelManager .up.|> dataModel +StorageManager .up.|> dataStorage -LogicManager --> Model -LogicManager --> Storage +LogicManager --> dataModel +LogicManager --> dataStorage @enduml diff --git a/docs/diagrams/DeleteSequenceDiagram.puml b/docs/diagrams/DeleteSequenceDiagram.puml index 1dc2311b245..ae27fa2d861 100644 --- a/docs/diagrams/DeleteSequenceDiagram.puml +++ b/docs/diagrams/DeleteSequenceDiagram.puml @@ -3,9 +3,9 @@ box Logic LOGIC_COLOR_T1 participant ":LogicManager" as LogicManager LOGIC_COLOR -participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR -participant ":DeleteCommandParser" as DeleteCommandParser LOGIC_COLOR -participant "d:DeleteCommand" as DeleteCommand LOGIC_COLOR +participant ":ExpenseTrackerParser" as AddressBookParser LOGIC_COLOR +participant ":DeleteExpenseCommandParser" as DeleteCommandParser LOGIC_COLOR +participant "d:DeleteExpenseCommand" as DeleteCommand LOGIC_COLOR participant ":CommandResult" as CommandResult LOGIC_COLOR end box @@ -48,7 +48,7 @@ deactivate AddressBookParser LogicManager -> DeleteCommand : execute() activate DeleteCommand -DeleteCommand -> Model : deletePerson(1) +DeleteCommand -> Model : deleteExpense(e) activate Model Model --> DeleteCommand diff --git a/docs/diagrams/LogicClassDiagram.puml b/docs/diagrams/LogicClassDiagram.puml index d4193173e18..07c3c7dc045 100644 --- a/docs/diagrams/LogicClassDiagram.puml +++ b/docs/diagrams/LogicClassDiagram.puml @@ -6,13 +6,13 @@ skinparam classBackgroundColor LOGIC_COLOR package Logic { -Class AddressBookParser +Class ExpenseTrackerParser Class XYZCommand Class CommandResult Class "{abstract}\nCommand" as Command -Class "<>\nLogic" as Logic +Class "<>\nLogic" as dataLogic Class LogicManager } @@ -26,9 +26,9 @@ package Storage{ Class HiddenOutside #FFFFFF HiddenOutside ..> Logic -LogicManager .right.|> Logic -LogicManager -right->"1" AddressBookParser -AddressBookParser ..> XYZCommand : creates > +LogicManager .right.|> dataLogic +LogicManager -right->"1" ExpenseTrackerParser +ExpenseTrackerParser ..> XYZCommand : creates > XYZCommand -up-|> Command LogicManager .left.> Command : executes > @@ -38,9 +38,9 @@ LogicManager --> Storage Storage --[hidden] Model Command .[hidden]up.> Storage Command .right.> Model -note right of XYZCommand: XYZCommand = AddCommand, \nFindCommand, etc +note right of XYZCommand: XYZCommand = AddExpenseCommand, \nFindCommand, etc -Logic ..> CommandResult +dataLogic ..> CommandResult LogicManager .down.> CommandResult Command .up.> CommandResult : produces > @enduml diff --git a/docs/diagrams/ModelClassDiagram.puml b/docs/diagrams/ModelClassDiagram.puml index 4439108973a..d623d45f6e1 100644 --- a/docs/diagrams/ModelClassDiagram.puml +++ b/docs/diagrams/ModelClassDiagram.puml @@ -5,46 +5,41 @@ skinparam arrowColor MODEL_COLOR skinparam classBackgroundColor MODEL_COLOR Package Model <>{ -Class "<>\nReadOnlyAddressBook" as ReadOnlyAddressBook +Class "<>\nReadOnlyExpenseTracker" as ReadOnlyExpenseTracker Class "<>\nReadOnlyUserPrefs" as ReadOnlyUserPrefs -Class "<>\nModel" as Model -Class AddressBook +Class "<>\nModel" as dataModel +Class ExpenseTracker Class ModelManager Class UserPrefs -Class UniquePersonList -Class Person -Class Address -Class Email -Class Name -Class Phone -Class Tag - +Class UniqueCategoryList +Class ExpenseList +Class RecurringExpenseList +Class Category +Class Expense +Class RecurringExpenseManager } Class HiddenOutside #FFFFFF HiddenOutside ..> Model -AddressBook .up.|> ReadOnlyAddressBook +ExpenseTracker .up.|> ReadOnlyExpenseTracker -ModelManager .up.|> Model -Model .right.> ReadOnlyUserPrefs -Model .left.> ReadOnlyAddressBook -ModelManager -left-> "1" AddressBook +ModelManager .up.|> dataModel +dataModel .right.> ReadOnlyUserPrefs +dataModel .left.> ReadOnlyExpenseTracker +ModelManager -down-> "1" ExpenseTracker ModelManager -right-> "1" UserPrefs UserPrefs .up.|> ReadOnlyUserPrefs -AddressBook *--> "1" UniquePersonList -UniquePersonList --> "~* all" Person -Person *--> Name -Person *--> Phone -Person *--> Email -Person *--> Address -Person *--> "*" Tag - -Name -[hidden]right-> Phone -Phone -[hidden]right-> Address -Address -[hidden]right-> Email +ExpenseTracker *--> "1" UniqueCategoryList +ExpenseTracker *--> "1" ExpenseList +ExpenseTracker *--> "1" RecurringExpenseList +UniqueCategoryList --> "~* all" Category +ExpenseList --> "~* all" Expense +RecurringExpenseList --> "~* all" RecurringExpenseManager -ModelManager -->"~* filtered" Person +ModelManager -->"~* filtered" Category +ModelManager -->"~* filtered" Expense +ModelManager -->"~* filtered" RecurringExpenseManager @enduml diff --git a/docs/diagrams/ParserClasses.puml b/docs/diagrams/ParserClasses.puml index 0c7424de6e0..09fbe9bee88 100644 --- a/docs/diagrams/ParserClasses.puml +++ b/docs/diagrams/ParserClasses.puml @@ -9,7 +9,7 @@ Class XYZCommand package "Parser classes"{ Class "<>\nParser" as Parser -Class AddressBookParser +Class ExpenseTrackerParser Class XYZCommandParser Class CliSyntax Class ParserUtil @@ -19,12 +19,12 @@ Class Prefix } Class HiddenOutside #FFFFFF -HiddenOutside ..> AddressBookParser +HiddenOutside ..> ExpenseTrackerParser -AddressBookParser .down.> XYZCommandParser: creates > +ExpenseTrackerParser .down.> XYZCommandParser: creates > XYZCommandParser ..> XYZCommand : creates > -AddressBookParser ..> Command : returns > +ExpenseTrackerParser ..> Command : returns > XYZCommandParser .up.|> Parser XYZCommandParser ..> ArgumentMultimap XYZCommandParser ..> ArgumentTokenizer diff --git a/docs/diagrams/StorageClassDiagram.puml b/docs/diagrams/StorageClassDiagram.puml index 760305e0e58..5ca0d26a0b4 100644 --- a/docs/diagrams/StorageClassDiagram.puml +++ b/docs/diagrams/StorageClassDiagram.puml @@ -11,15 +11,17 @@ Class "<>\nUserPrefsStorage" as UserPrefsStorage Class JsonUserPrefsStorage } -Class "<>\nStorage" as Storage +Class "<>\nStorage" as dataStorage Class StorageManager -package "AddressBook Storage" #F4F6F6{ -Class "<>\nAddressBookStorage" as AddressBookStorage -Class JsonAddressBookStorage -Class JsonSerializableAddressBook -Class JsonAdaptedPerson -Class JsonAdaptedTag +package "ExpenseTracker Storage" #F4F6F6{ +Class "<>\nExpenseTrackerStorage" as ExpenseTrackerStorage +Class JsonExpenseTrackerStorage +Class JsonSerializableExpenseTracker +Class JsonAdaptedExpense +Class JsonAdaptedCategory +Class JsonAdaptedRecurringExpense +Class JsonAdaptedBudget } } @@ -27,17 +29,19 @@ Class JsonAdaptedTag Class HiddenOutside #FFFFFF HiddenOutside ..> Storage -StorageManager .up.|> Storage +StorageManager .up.|> dataStorage StorageManager -up-> "1" UserPrefsStorage -StorageManager -up-> "1" AddressBookStorage +StorageManager -up-> "1" ExpenseTrackerStorage -Storage -left-|> UserPrefsStorage -Storage -right-|> AddressBookStorage +dataStorage -left-|> UserPrefsStorage +dataStorage -right-|> ExpenseTrackerStorage JsonUserPrefsStorage .up.|> UserPrefsStorage -JsonAddressBookStorage .up.|> AddressBookStorage -JsonAddressBookStorage ..> JsonSerializableAddressBook -JsonSerializableAddressBook --> "*" JsonAdaptedPerson -JsonAdaptedPerson --> "*" JsonAdaptedTag +JsonExpenseTrackerStorage .up.|> ExpenseTrackerStorage +JsonExpenseTrackerStorage ..> JsonSerializableExpenseTracker +JsonSerializableExpenseTracker --> "*" JsonAdaptedExpense +JsonSerializableExpenseTracker --> "*" JsonAdaptedCategory +JsonSerializableExpenseTracker --> "*" JsonAdaptedRecurringExpense +JsonSerializableExpenseTracker --> "*" JsonAdaptedBudget @enduml diff --git a/docs/diagrams/UiClassDiagram.puml b/docs/diagrams/UiClassDiagram.puml index 95473d5aa19..d5f2d2245cd 100644 --- a/docs/diagrams/UiClassDiagram.puml +++ b/docs/diagrams/UiClassDiagram.puml @@ -9,11 +9,20 @@ Class "<>\nUi" as Ui Class "{abstract}\nUiPart" as UiPart Class UiManager Class MainWindow +Class CategoryCard +Class ExpenseCard +Class ExpenseListPanel +Class RecurringExpenseCard +Class RecurringExpenseListPanel Class HelpWindow Class ResultDisplay -Class PersonListPanel -Class PersonCard +Class CategoryListPanel +Class SuggestionCard +Class SuggestionListPanel +Class StatisticsPanel Class StatusBarFooter +Class ResultDetails +Class ResultHeader Class CommandBox } @@ -21,40 +30,57 @@ package Model <> { Class HiddenModel #FFFFFF } +package AnalyticModel <> { +Class HiddenModel #FFFFFF +} + package Logic <> { Class HiddenLogic #FFFFFF } Class HiddenOutside #FFFFFF -HiddenOutside ..> Ui UiManager .left.|> Ui -UiManager -down-> "1" MainWindow +UiManager -> "1" MainWindow MainWindow *-down-> "1" CommandBox +MainWindow *-right> "1" StatisticsPanel MainWindow *-down-> "1" ResultDisplay -MainWindow *-down-> "1" PersonListPanel +MainWindow *-down-> "1" ExpenseListPanel +MainWindow *-down-> "1" CategoryListPanel +MainWindow *-down-> "1" RecurringExpenseListPanel MainWindow *-down-> "1" StatusBarFooter -MainWindow --> "0..1" HelpWindow +MainWindow *-down-> "1" ResultHeader +MainWindow *-down-> "1" ResultDetails +MainWindow *-down-> "1" SuggestionListPanel +MainWindow -down-> "0..1" HelpWindow -PersonListPanel -down-> "*" PersonCard +CategoryListPanel -down-> "*" CategoryCard +ExpenseListPanel -down-> "*" ExpenseCard +RecurringExpenseListPanel -down-> "*" RecurringExpenseCard +SuggestionListPanel -down-> "*" SuggestionCard MainWindow -left-|> UiPart ResultDisplay --|> UiPart CommandBox --|> UiPart -PersonListPanel --|> UiPart -PersonCard --|> UiPart StatusBarFooter --|> UiPart HelpWindow --|> UiPart +CategoryListPanel --|> UiPart +CategoryCard --|> UiPart +ExpenseListPanel --|> UiPart +ExpenseCard --|> UiPart +RecurringExpenseListPanel --|> UiPart +RecurringExpenseCard --|> UiPart +SuggestionListPanel --|> UiPart +SuggestionCard --|> UiPart -PersonCard ..> Model -UiManager -right-> Logic -MainWindow -left-> Logic +UiManager -up-> Logic +MainWindow -up-> Logic +StatisticsPanel -up-> AnalyticModel +AnalyticModel -right-> Model -PersonListPanel -[hidden]left- HelpWindow HelpWindow -[hidden]left- CommandBox -CommandBox -[hidden]left- ResultDisplay -ResultDisplay -[hidden]left- StatusBarFooter +ResultHeader -[hidden]left- ResultDisplay MainWindow -[hidden]-|> UiPart @enduml diff --git a/docs/diagrams/activity_diagrams/addRecurringExpenseActivityDiagram.puml b/docs/diagrams/activity_diagrams/addRecurringExpenseActivityDiagram.puml new file mode 100644 index 00000000000..7f72164fbb3 --- /dev/null +++ b/docs/diagrams/activity_diagrams/addRecurringExpenseActivityDiagram.puml @@ -0,0 +1,31 @@ +@startuml +start +:User executes "addrec" command; +:"addrec" command is parsed; + +if () then ([arguments present & in valid format]) + if () then ([end date later than start date]) + :RecurringExpenseManager object created; + :AddRecurringExpenseCommand object created and executed; + if () then ([recurring expense is unique) + :Search category list for instance matching category name; + if () then ([matching instance found]) + :Link matching category to RecurringExpenseManager; + else ([else]) + :Add category to category list; + endif; + :Add RecurringExpenseManager object to recurring expense list; + :Generate new expenses from recurring expense; + :Display success message; + else ([else]) + :Error message displayed specifying duplicate recurring expense; + endif + else ([else]) + :Error message displayed specifying end date was earlier than start date; + endif + +else ([else]) + :Error message displayed specifying an error in the command format; +endif +stop +@enduml diff --git a/docs/diagrams/activity_diagrams/deleteRecurringExpenseActivityDiagram.puml b/docs/diagrams/activity_diagrams/deleteRecurringExpenseActivityDiagram.puml new file mode 100644 index 00000000000..603de8d0947 --- /dev/null +++ b/docs/diagrams/activity_diagrams/deleteRecurringExpenseActivityDiagram.puml @@ -0,0 +1,18 @@ +@startuml +start +:User executes "delrec" command; +:"delrec" command is parsed; + +if () then ([index is present]) + if () then ([index is within range]) + :Get RecurringExpenseManager instance to edit; + :Delete RecurringExpenseManager from recurring expense list; + :Display success message; + else ([else]) + :Error message displayed specifying index out of range; + endif; +else ([else]) + :Error message displayed specifying an error in the command format; +endif +stop +@enduml diff --git a/docs/diagrams/activity_diagrams/editRecurringExpenseActivityDiagram.puml b/docs/diagrams/activity_diagrams/editRecurringExpenseActivityDiagram.puml new file mode 100644 index 00000000000..1653cc6dd45 --- /dev/null +++ b/docs/diagrams/activity_diagrams/editRecurringExpenseActivityDiagram.puml @@ -0,0 +1,24 @@ +@startuml +start +:User executes "edrec" command; +:"edrec" command is parsed; + +if () then ([at least one argument is present]) + if () then ([arguments in valid format and index within range]) + :Get RecurringExpenseManager instance to edit; + :Search category list for instance matching category name; + if () then ([matching instance found]) + :Link matching category to RecurringExpenseManager; + :Update necessary attributes in RecurringExpenseManager; + :Display success message; + else ([else]) + :Error message displayed specifying nonexistent category; + endif; + else ([else]) + :Error message displayed specifying an error in the command format or index out of range; + endif +else ([else]) + :Error message displayed specifying no attributes given to edit; +endif +stop +@enduml diff --git a/docs/diagrams/style.puml b/docs/diagrams/style.puml index fad8b0adeaa..2a3b44b9858 100644 --- a/docs/diagrams/style.puml +++ b/docs/diagrams/style.puml @@ -55,8 +55,9 @@ skinparam Sequence { MessageAlign center BoxFontSize 15 BoxPadding 0 - BoxFontColor #FFFFFF + BoxFontColor #000000 FontName Arial + ReferenceBackgroundColor #FFFFFF } skinparam Participant { diff --git a/docs/diagrams/uml/commands.puml b/docs/diagrams/uml/commands.puml new file mode 100644 index 00000000000..090bd8252a9 --- /dev/null +++ b/docs/diagrams/uml/commands.puml @@ -0,0 +1,221 @@ +@startuml commands +skinparam defaultTextAlignment left +' scale 0.6 +hide empty members +hide circle +skinparam classAttributeIconSize 0 + + +interface Command { + + abstract execute(dataModel: DataModel): CommandResult +} + +class CommandResult { + + ComandResult(feedbackToUser: String, screenType: ScreenType) + + CommandResult(feedbackToUser: String, screenType: ScreenType, \ + showHelp: boolean, exit: boolean) + + getFeedbackToUser(): String + + getScreenType(): ScreenType + + isShowHelp(): boolean + + isExit(): boolean + - feedbackToUser: String + - screenType: ScreenType + - showHelp: boolean + - exit: boolean +} + +interface AddCommand extends Command { +} + +interface DeleteCommand extends Command { +} + +interface EditCommand extends Command { +} + +interface ListCommand extends Command { +} + +interface GeneralCommand extends Command { +} + +class SetBudgetCommand { + + SetBudgetCommand(budget: Budget) + - budget: Budget +} + +Command <.. SetBudgetCommand +Command "1" *-- "1" CommandResult + +@enduml + +@startuml add_commands + +skinparam defaultTextAlignment left +' scale 0.6 +hide empty members +hide circle +skinparam classAttributeIconSize 0 + +' COMMAND_WORD, MESSAGE_USAGE< MESSSAGE_SUCCESS, MESSAGE_DUPLICATE_CATEGORY are not shown +interface AddCommand { + + abstract execute(dataModel: DataModel): CommandResult +} + +class AddCategoryCommand {} + +class AddExpenseCommand { + + AddExpenseCommand(expense: Expense) + - expense: Expense +} + +class AddRecurringExpenseCommand { + + AddRecurringExpenseCommand(toAdd: RecurringExpenseManager) + - toAdd: RecurringExpenseManager +} + +class CategorySummaryCommand { + + CategorySummaryCommand(index: Index) + - index: Index +} + +AddCommand <|-- AddCategoryCommand +AddCommand <|-- AddExpenseCommand +AddCommand <|-- AddRecurringExpenseCommand + +@enduml + +@startuml delete_commands + +skinparam defaultTextAlignment left +' scale 0.6 +hide empty members +hide circle +skinparam classAttributeIconSize 0 + +interface DeleteCommand {} + +class DeleteCategoryCommand { + + DeleteCategoryCommand(index: Index) + - index: Index +} + +class DeleteExpenseCommand { + + DeleteExpenseCommand(index: Index) + - index: Index +} + +class DeleteRecurringExpenseCommand { + + DeleteRecurringExpenseCommand(index: Index) + - index: Index +} + +DeleteCommand <.. DeleteCategoryCommand +DeleteCommand <.. DeleteExpenseCommand +DeleteCommand <.. DeleteRecurringExpenseCommand + +@enduml + +@startuml edit_commands + +skinparam defaultTextAlignment left +' scale 0.6 +hide empty members +hide circle +skinparam classAttributeIconSize 0 + +interface EditCommand {} + +class EditCategoryCommand { + + EditCategoryCommand(index: Index, newCategoryName: String, newSummary: String) + - index: Index + - newCategoryName: String + - newSummary: String +} + +class EditExpenseCommand { + ' + EditExpenseCommand(index: Index, newExpenseName: String, newExpenseAmount: Double, \ + newExpenseDate: LocalDate, newExpenseCategory: Category) ' + - index: Index + - newExpenseName: String + - newExpenseAmount: Double + - newExpenseDate: LocalDate + - newExpenseCategory: Category +} + +class EditRecurringExpenseManagerCommand { + ' + EditRecurringExpenseManagerCommand(index: Index, newExpenseName: String, \ + newExpenseAmount: Double, newExpenseCategoryInString: String, \ + newExpenseEndDate: LocalDate, newFrequencyInString: String) ' + - index: Index + - newExpenseName: String + - newExpenseAmount: Double + - newExpenseCategoryInString: String + - newExpenseEndDate: LocalDate + - newFrequencyInString: String +} + + +EditCommand <.. EditCategoryCommand +EditCommand <.. EditExpenseCommand +EditCommand <.. EditRecurringExpenseManagerCommand + +@enduml + +@startuml list_commands + +skinparam defaultTextAlignment left +' scale 0.6 +hide empty members +hide circle +skinparam classAttributeIconSize 0 + +interface ListCommand {} + +class ListCategoryCommand {} + +class ListExpensesCommand { + ' + ListCategoriesCommand(categoryPredicate: Optional, \ + timespanPredicate: Optional) ' + - categoryPredicate: Optional + - timespanPredicate: Optional +} + +class ListRecurringExpenseCommand {} + + +ListCommand <.. ListCategoryCommand +ListCommand <.. ListExpensesCommand +ListCommand <.. ListRecurringExpenseCommand + +@enduml + +@startuml general_commands + +skinparam defaultTextAlignment left +' scale 0.6 +hide empty members +hide circle +skinparam classAttributeIconSize 0 + +interface GeneralCommand {} + +class ClearCommand {} + +class ExitCommand {} + +class FindCommand { + + FindCommand(predicate: ExpenseContainsKeywordsPredicate) + - predicate: ExpenseContainsKeywordsPredicate +} + +class HelpCommand {} + +GeneralCommand <.. CategorySummaryCommand +GeneralCommand <.. ClearCommand +GeneralCommand <.. ExitCommand +GeneralCommand <.. FindCommand +GeneralCommand <.. HelpCommand + +@enduml + diff --git a/docs/diagrams/uml/model.puml b/docs/diagrams/uml/model.puml new file mode 100644 index 00000000000..d02864fdf93 --- /dev/null +++ b/docs/diagrams/uml/model.puml @@ -0,0 +1,227 @@ +@startuml model +hide empty members +hide circle +skinparam classAttributeIconSize 0 + +class Budget { + + Budget(budget: double) + + getMonthlyBudget(): double + + getWeeklyBudget(): double + + setMonthlyBudget(monthBudget: double): void + - monthBudget: double +} + +abstract class Category { + + Category(categoryName: String, summary: String) + + getCategoryName(): String + + getSummary(): String + + isValidCategoryName(categoryName: String): boolean + # categoryName: String + # summary: String +} + +class MiscellaneousCategory extends Category { + + MiscellaneousCategory() +} + +class UserDefinedCategory extends Category { + + UserDefinedCategory(categoryName: String, summary: String) + + setCategoryName(categoryName: String): void + + setDescription(summary: String): void +} + +class UniqueCategoryList { + + contains(category: Category): boolean + + add(newCategory: Category): void + + remove(category: Category): void + + clear(): void + + setCategoryList(listOfCategories: List): void + + categoriesAreUnique(listOfCategories: List): boolean + + asUnmodifiableList(): ObservableList + - internalListOfCategories: ObservableList + - internalUnmodifiableList: ObservableList +} + +' getters and setters are not shown +class Expense { + + Expense(name: String, amount: Price, date: LocalDate, category: Category) + + getFormattedDate(): String + + {static} isValidName(name: String): boolean + + {static} MESSAGE_CONSTRAINTS: String + + {static} VALIDATION_REGEX: String + - name: String + - amount: Price + - date: LocalDate + - category: Category +} + +class Price { + + Price(amount: String) + + getPriceAsDouble(): double + + {static} isValidPrice(amount: String): boolean + + {static} MESSAGE_CONSTRAINTS: String + + {static} VALIDATION_REGEX: String + - amount: String +} + +class ExpenseList { + + add(newExpense: Expense): void + + remove(expense: Expense): void + + clear(): void + + set(index: int, expense: Expense): void + + replaceDeletedCategory(target: Category): void + + contains(expense: Expense): boolean + + setExpenseList(listOfExpenses: List): void + + getSize(): int + + getTotalAmount(): double + + sortList(): void + + asUnmodifiableList(): ObservableList + - internalListOfExpenses: ObservableList + - internalUnmodifiableList: ObservableList + - misc: MiscellaneousCategory +} + +class RecurringExpenseList { + + addRecurringExpense(recurringExpense: RecurringExpenseManager): void + + removeRecurringExpense(recurringExpense: RecurringExpenseManager): void + + getExpenses(): ArrayList + + cleanupExpiredGenerators(): void + + getSize(): int + : getTotalAmount(): double + + asUnmodifiableList(): ObservableList + - recurringExpenseList: ObservableList + - internalUnmodifiableList: ObservableList +} + +class RecurringExpenseManager { + + RecurringExpenseManager(expenseName: String, amount: Price, expenseCategory: Category, numberOfExpenses: int, nextExpenseDate: LocalDate, startDate: LocalDate, endDate: LocalDate, recurringExpenseType: RecurringExpenseType) + + getExpenses(): ArrayList + - expenseName: String + - amount: Price + - expenseCategory: Category + - numberOfExpenses: int + - nextExpenseDate: LocalDate + - startDate: LocalDate + - endDate: LocalDate + - recurringExpenseType: RecurringExpenseType +} + +enum RecurringExpenseType { + + DAILY + + WEEKLY + + MONTHLY + + YEARLY + + getNextExpenseDate(currentDate: LocalDate): LocalDate +} + +class ExpenseContainsKeywordsPredicate { + + ExpenseContainsKeywordsPredicate(keywords: List) + - keywords: List +} + +class ExpenseInTimespanPredicate { + + ExpenseInTimespanPredicate(timespan: Timespan) + - timespan: Timespan +} + +class ExpenseInCategoryPredicate { + + ExpenseInCategoryPredicate(category: Category) + - category: Category +} + +enum AnalyticsType { + + MONTHLY_SPENT + + WEEKLY_SPENT + + TOTAL_SPENT + + MONTHLY_REMAINING + + WEEKLY_REMAINING + + WEEKLY_CHANGE + + MONTHLY_CHANGE + + BUDGET_PERCENTAGE +} + +interface AnalyticModel { + + getAnalyticsData(type: AnalyticsType): double + + getMonthlyBudget(): DoubleProperty + + getMonthlyRemaining(): DoubleProperty + + getWeeklySpent(): DoubleProperty + + getWeeklyRemaining(): DoubleProperty + + getWeeklyChange(): DoubleProperty + + getMonthlyChange(): DoubleProperty + + getTotalSpent(): DoubleProperty + + getBudgetPercentage(): DoubleProperty + + updateMonthlyBudgetProperty(newBudget: Budget): void + + updateWeeklyBudgetProperty(newBudget: Budget): void + + updateAllStatistics(): void +} + +class AnalyticsModelManager { + + AnalyticsModelManager(expenseTracker: ExpenseTracker, referenceDate: LocalDate) +} + + +interface ReadOnlyExpenseTracker { + + getExpenseList(): ObservableList + + getRecurringExpenseGenerators(): ObservableList + + getCategoryList(): ObservableList + + getBudgetForStats(): ObjectProperty + + getBudget(): Budget +} + +' add, has, and remove methods are not shown +class ExpenseTracker { + + ExpenseTracker() + + ExpenseTracker(toBeCopied: ReadOnlyExpenseTracker) + + resetData(newData: ReadOnlyExpenseTracker): void + + generateRetroactiveExpenses(): void + + cleanUpRecurrenceGenerators(): void + - expenses: ExpenseList + - recurringGenerators: RecurringExpenseList + - categories: UniqueCategoryList + - simpleBudget: ObjectProperty +} + +interface ReadOnlyUserPrefs { + + getGuiSettings(): GuiSettings + + getExpenseTrackerFilePath(): Path +} + +class UserPrefs { + + UserPrefs(UserPrefs: : ReadOnlyUserPrefs) + + resetData(newUserPrefs: ReadOnlyUserPrefs): void + - guiSettings: GuiSettings + - expenseTrackerFilePath: Path +} + +ReadOnlyExpenseTracker <|-- ExpenseTracker +ReadOnlyUserPrefs <|-- UserPrefs +ExpenseTracker *-- ExpenseList +ExpenseTracker *-- RecurringExpenseList +ExpenseTracker *-- UniqueCategoryList +ExpenseTracker *-- Budget +ExpenseList *-- Expense +RecurringExpenseList *-- RecurringExpenseManager +UniqueCategoryList *-- Category +Category <|-- MiscellaneousCategory +Category <|-- UserDefinedCategory +Expense *-- Price +Expense *-- Category +RecurringExpenseManager *-- Price +RecurringExpenseManager *-- Category +RecurringExpenseManager *-- RecurringExpenseType +ExpenseList *-- ExpenseContainsKeywordsPredicate +ExpenseList *-- ExpenseInTimespanPredicate +ExpenseList *-- ExpenseInCategoryPredicate +AnalyticModel <|-- AnalyticsModelManager +AnalyticsModelManager *-- ExpenseTracker +AnalyticsModelManager *-- LocalDate +AnalyticsModelManager *-- Budget +AnalyticsModelManager *-- AnalyticsType +AnalyticsModelManager *-- DoubleProperty +AnalyticsModelManager *-- ObjectProperty +AnalyticsModelManager *-- ReadOnlyExpenseTracker +AnalyticsModelManager *-- ReadOnlyUserPrefs +AnalyticsModelManager *-- GuiSettings +AnalyticsModelManager *-- Path + +@enduml diff --git a/docs/diagrams/uml/parser.puml b/docs/diagrams/uml/parser.puml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/docs/diagrams/uml/sequence_diagrams/RecurringExpenseStartUpSequenceDiagram.puml b/docs/diagrams/uml/sequence_diagrams/RecurringExpenseStartUpSequenceDiagram.puml new file mode 100644 index 00000000000..add7df7af4c --- /dev/null +++ b/docs/diagrams/uml/sequence_diagrams/RecurringExpenseStartUpSequenceDiagram.puml @@ -0,0 +1,46 @@ +@startuml +!include ../../style.puml + +participant ":Mainapp" as Main #097969 + +box Model MODEL_COLOR_T1 +participant "dataModel:Model" as dataModel MODEL_COLOR +participant "expenseTracker:ExpenseTracker" as ExpenseTracker MODEL_COLOR +participant ":RecurringExpenseManager" as Manager MODEL_COLOR +end box + + +create dataModel +Main -> dataModel +activate dataModel + +create ExpenseTracker +dataModel -> ExpenseTracker +activate ExpenseTracker + +ExpenseTracker -> ExpenseTracker : resetData() +activate ExpenseTracker +ExpenseTracker -> ExpenseTracker : generateRetroactiveExpenses() +loop for each RecurringExpenseManager + activate ExpenseTracker + ExpenseTracker -> Manager : getExpenses() + activate Manager + Manager --> ExpenseTracker : list of recurring expenses + deactivate Manager + ref over ExpenseTracker + add expenses in the list + to FastTrack + end ref +end +ExpenseTracker --> ExpenseTracker +deactivate ExpenseTracker + +ExpenseTracker --> ExpenseTracker +deactivate ExpenseTracker +ExpenseTracker --> dataModel +deactivate ExpenseTracker + +Main <-- dataModel +deactivate dataModel + +@enduml diff --git a/docs/diagrams/uml/sequence_diagrams/SetBudgetSequenceDiagram.puml b/docs/diagrams/uml/sequence_diagrams/SetBudgetSequenceDiagram.puml new file mode 100644 index 00000000000..a83aa7e5021 --- /dev/null +++ b/docs/diagrams/uml/sequence_diagrams/SetBudgetSequenceDiagram.puml @@ -0,0 +1,37 @@ +@startuml +!include ../../style.puml + +box Logic LOGIC_COLOR_T1 +participant ":SetBudgetCommand" as SetBudgetCommand LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant "dataModel:Model" as Model MODEL_COLOR +participant "expenseTracker:ExpenseTracker" as ExpenseTracker MODEL_COLOR +end box + +[-> SetBudgetCommand : execute(dataModel) +activate SetBudgetCommand + +SetBudgetCommand -> Model : setBudget(budget) +activate Model + +Model -> ExpenseTracker : setBudget(budget) +activate ExpenseTracker + +ref over ExpenseTracker + Set simpleBudget field + in ExpenseTracker +end ref + +ExpenseTracker --> Model +deactivate ExpenseTracker + +Model --> SetBudgetCommand +deactivate Model + +[<-- SetBudgetCommand : CommandResult +deactivate SetBudgetCommand +destroy SetBudgetCommand + +@enduml diff --git a/docs/diagrams/uml/sequence_diagrams/add_expense_command.puml b/docs/diagrams/uml/sequence_diagrams/add_expense_command.puml new file mode 100644 index 00000000000..1243042bc96 --- /dev/null +++ b/docs/diagrams/uml/sequence_diagrams/add_expense_command.puml @@ -0,0 +1,91 @@ +@startuml AddExpenseSequenceDiagram +!include ../../style.puml + +hide footbox +skinparam MinClassWidth 50 +skinparam ParticipantPadding 10 +skinparam Shadowing false +skinparam DefaultTextAlignment center +skinparam packageStyle Rectangle + +box Model MODEL_COLOR_T1 +participant "newExpense: Expense" as Expense order 2 MODEL_COLOR +participant "dataModel: Model" as Model order 3 MODEL_COLOR +participant "expenseTracker: ExpenseTracker" as ExpenseTracker order 4 MODEL_COLOR +participant "expenses: ExpenseList" as ExpenseList order 5 MODEL_COLOR +end box + +box Logic LOGIC_COLOR_T1 +participant ":AddExpenseCommand" as Command LOGIC_COLOR +participant ":CommandResult" as Result LOGIC_COLOR +end box + +[->Command: execute(dataModel) +activate Command +Command -> Expense: getCategory() +activate Expense +Expense --> Command: newCategory +deactivate Expense +Command -> Model : getCategoryInstance(newCategory) +ref over Model, ExpenseTracker + get existing category +end ref +Model --> Command: existingCategory +alt existingCategory != null + Command -> Expense: setCategory(existingCategory) + activate Expense + Expense -> Model + ref over Model + sets category to + link to existing one. + end ref + Model --> Expense + Expense --> Command + deactivate Expense +else else + Command -> Model + activate Model + Model -> ExpenseTracker : addCategory(toAdd) + activate ExpenseTracker + + ExpenseTracker -> ExpenseList + ref over ExpenseList + add newCategory to + ObservableList + end ref + ExpenseList --> ExpenseTracker + ExpenseTracker --> Model + deactivate ExpenseTracker + Model --> Command + deactivate Model +end +Command -> Model: addExpense(newExpense) +activate Model +Model -> ExpenseTracker: addExpense(newExpense) +activate ExpenseTracker +ExpenseTracker -> ExpenseList: add(expense) +activate ExpenseList +ExpenseList -> ExpenseList: add newExpense to \nObservableList +ExpenseList --> ExpenseTracker +deactivate ExpenseList +ExpenseTracker -> ExpenseList: sortList() +activate ExpenseList +ExpenseList -> ExpenseList: sort expenses in \nObservableList\nby date +ExpenseList --> ExpenseTracker +deactivate ExpenseList +ExpenseTracker --> Model +deactivate ExpenseTracker +Model -> Model: updateFilteredExpenseList\n(PREDICATE_SHOW_ALL_EXPENSES) +activate Model +deactivate Model +Model --> Command +deactivate Model +create Result +Command -> Result +activate Result +Result --> Command +deactivate Result +[<-- Command: result +destroy Command + +@enduml diff --git a/docs/diagrams/uml/sequence_diagrams/delete_expense_command.puml b/docs/diagrams/uml/sequence_diagrams/delete_expense_command.puml new file mode 100644 index 00000000000..bacf1680dab --- /dev/null +++ b/docs/diagrams/uml/sequence_diagrams/delete_expense_command.puml @@ -0,0 +1,65 @@ +@startuml DeleteExpenseSequenceDiagram +!include ../../style.puml + +hide footbox +skinparam MinClassWidth 50 +skinparam ParticipantPadding 10 +skinparam Shadowing false +skinparam DefaultTextAlignment center +skinparam packageStyle Rectangle + +box Model MODEL_COLOR_T1 +participant "dataModel: Model" as Model order 3 MODEL_COLOR +participant "expenseTracker: ExpenseTracker" as ExpenseTracker order 4 MODEL_COLOR +participant "expenses: ExpenseList" as ExpenseList order 5 MODEL_COLOR +end box + +box Logic LOGIC_COLOR_T1 +participant ":DeleteExpenseCommand" as Command LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +[->Command: execute(dataModel) +activate Command +Command -> Model: getFilteredExpenseList() +activate Model +Model --> Command: lastShownList +deactivate Model + +Command -> Model +ref over Model + get expense from + lastShownList using parsed index +end ref +Model --> Command + +Command -> Model: deleteExpense(expense) +activate Model +Model -> ExpenseTracker: deleteExpense(expense) +activate ExpenseTracker +ExpenseTracker -> ExpenseList: remove(expense) +activate ExpenseList +ExpenseList -> ExpenseList: remove expense from \nObservableList +ExpenseList --> ExpenseTracker +deactivate ExpenseList +ExpenseTracker -> ExpenseList: sortList() +activate ExpenseList +ExpenseList -> ExpenseList: sort expenses in \nObservableList\nby date +ExpenseList --> ExpenseTracker +deactivate ExpenseList +ExpenseTracker --> Model +deactivate ExpenseTracker +Model -> Model: updateFilteredExpenseList\n(PREDICATE_SHOW_ALL_EXPENSES) +activate Model +deactivate Model +Model --> Command +deactivate Model +create CommandResult +Command -> CommandResult +activate CommandResult +CommandResult --> Command +deactivate CommandResult +[<-- Command: result +destroy Command + +@enduml diff --git a/docs/diagrams/uml/sequence_diagrams/edit_exense_command_part.puml b/docs/diagrams/uml/sequence_diagrams/edit_exense_command_part.puml new file mode 100644 index 00000000000..1a1cb9c4729 --- /dev/null +++ b/docs/diagrams/uml/sequence_diagrams/edit_exense_command_part.puml @@ -0,0 +1,35 @@ +@startuml EditExpenseFindPartSequenceDiagram +!include ../../style.puml + +hide footbox +skinparam MinClassWidth 50 +skinparam ParticipantPadding 10 +skinparam Shadowing false +skinparam DefaultTextAlignment center +skinparam packageStyle Rectangle + +box Logic LOGIC_COLOR_T1 +participant ":EditExpenseCommand" as Command order 1 LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant "category: Category" as Category order 2 MODEL_COLOR +end box + +loop until matching category is found + create Category + Command -> Category + activate Category + Category -[hidden]> Command + deactivate Category + + Command-[hidden]->Category + activate Category + Category -> Category :equals(newExpenseCategoryInString) + opt if equals check is true + Category -[hidden]> Category + Category --> Command + end + deactivate Category +end +@enduml diff --git a/docs/diagrams/uml/sequence_diagrams/edit_expense_command.puml b/docs/diagrams/uml/sequence_diagrams/edit_expense_command.puml new file mode 100644 index 00000000000..b53ccb4a870 --- /dev/null +++ b/docs/diagrams/uml/sequence_diagrams/edit_expense_command.puml @@ -0,0 +1,75 @@ +@startuml EditExpenseSequenceDiagram +!include ../../style.puml + +hide footbox +skinparam MinClassWidth 50 +skinparam ParticipantPadding 10 +skinparam Shadowing false +skinparam DefaultTextAlignment center +skinparam packageStyle Rectangle + +box Model MODEL_COLOR_T1 +participant "dataModel: Model" as Model order 3 MODEL_COLOR +participant "expenseTracker: ExpenseTracker" as ExpenseTracker order 4 MODEL_COLOR +participant "expenses: ExpenseList" as ExpenseList order 5 MODEL_COLOR +end box + +box Logic LOGIC_COLOR_T1 +participant ":EditExpenseCommand" as Command LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +[->Command: execute(dataModel) +activate Command +Command -> Model: getFilteredExpenseList() +activate Model +Model --> Command: lastShownExpenseList +deactivate Model +Command -> Model: getFilteredCategoryList() +activate Model +Model --> Command: lastShownCategoryList +deactivate Model + +Command -> Model +ref over Model + Look for matching category in + lastShownCategoryList and + retrieve the category list +end ref +ref over Model + Obtain the expense from lastShownExpenseList + using the parsed index + Create new expense with the new values +end ref +Model --> Command + +Command -> Model: setExpense(target, editedExpense) +activate Model +Model -> ExpenseTracker: setExpense(target, editedExpense) +activate ExpenseTracker +ExpenseTracker -> ExpenseList: setExpense(target, editedExpense) +activate ExpenseList +ExpenseList -> ExpenseList: retrieve target's index from \nObservableList, \nset editedExpense at index +ExpenseList --> ExpenseTracker +deactivate ExpenseList +ExpenseTracker -> ExpenseList: sortList() +activate ExpenseList +ExpenseList -> ExpenseList: sort expenses in \nObservableList\nby date +ExpenseList --> ExpenseTracker +deactivate ExpenseList +ExpenseTracker --> Model +deactivate ExpenseTracker +Model -> Model: updateFilteredExpenseList\n(PREDICATE_SHOW_ALL_EXPENSES) +activate Model +deactivate Model +Model --> Command +deactivate Model +create CommandResult +Command -> CommandResult +activate CommandResult +CommandResult --> Command +deactivate CommandResult +' [<-- Command: result +destroy Command + +@enduml diff --git a/docs/diagrams/uml/sequence_diagrams/find_command.puml b/docs/diagrams/uml/sequence_diagrams/find_command.puml new file mode 100644 index 00000000000..41de3a0a4e7 --- /dev/null +++ b/docs/diagrams/uml/sequence_diagrams/find_command.puml @@ -0,0 +1,35 @@ +@startuml FindSequenceDiagram +!include ../../style.puml + +box Logic LOGIC_COLOR_T1 +participant ":FindCommand" as FindCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant "model:Model" as Model order 3 MODEL_COLOR +end box + +[-> FindCommand : execute(model) +activate FindCommand +FindCommand -> Model : updateFilteredPersonList(predicate) +activate Model +deactivate Model +create CommandResult +FindCommand -> CommandResult + +activate CommandResult +CommandResult --> FindCommand +deactivate CommandResult +' FindCommand --> [ : result +[<--FindCommand : result +deactivate FindCommand +destroy FindCommand +hide footbox +skinparam MinClassWidth 50 +skinparam ParticipantPadding 10 +skinparam Shadowing false +skinparam DefaultTextAlignment center +skinparam packageStyle Rectangle + +@enduml diff --git a/docs/diagrams/uml/sequence_diagrams/list_category_command.puml b/docs/diagrams/uml/sequence_diagrams/list_category_command.puml new file mode 100644 index 00000000000..9e7fecfb3f9 --- /dev/null +++ b/docs/diagrams/uml/sequence_diagrams/list_category_command.puml @@ -0,0 +1,44 @@ +@startuml ListCategorySequenceDiagram +!include ../../style.puml + +hide footbox +skinparam MinClassWidth 50 +skinparam ParticipantPadding 10 +skinparam Shadowing false +skinparam DefaultTextAlignment center +skinparam packageStyle Rectangle + + + +box Logic LOGIC_COLOR_T1 +participant ":ListCategoryCommand" as ListCategoryCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant "model:Model" as Model order 3 MODEL_COLOR +end box + +[-> ListCategoryCommand : execute(model) +activate ListCategoryCommand +ListCategoryCommand -> Model : updateFilteredExpenseList(predicate) +activate Model +deactivate Model +create CommandResult +ListCategoryCommand -> CommandResult + +activate CommandResult +CommandResult --> ListCategoryCommand +deactivate CommandResult +[<--ListCategoryCommand : result +deactivate ListCategoryCommand +destroy ListCategoryCommand +hide footbox +skinparam MinClassWidth 50 +skinparam ParticipantPadding 10 +skinparam Shadowing false +skinparam DefaultTextAlignment center +skinparam packageStyle Rectangle + + +@enduml diff --git a/docs/diagrams/uml/sequence_diagrams/list_expenses_command.puml b/docs/diagrams/uml/sequence_diagrams/list_expenses_command.puml new file mode 100644 index 00000000000..8d0b789ef05 --- /dev/null +++ b/docs/diagrams/uml/sequence_diagrams/list_expenses_command.puml @@ -0,0 +1,83 @@ +@startuml ListExpensesSequenceDiagram +!include ../../style.puml + +hide footbox +skinparam MinClassWidth 50 +skinparam ParticipantPadding 10 +skinparam Shadowing false +skinparam DefaultTextAlignment center +skinparam packageStyle Rectangle + + +box Model MODEL_COLOR_T1 +participant "model:Model" as Model order 4 MODEL_COLOR +participant "filteredExpensesin\nExpenseTracker" as filterModel order 3 MODEL_COLOR +end box + +box Logic LOGIC_COLOR_T1 +participant ":ListExpenseCommand" as ListExpenseCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +[-> ListExpenseCommand : execute(model) +activate ListExpenseCommand +alt category predicate \nis present + ListExpenseCommand -> filterModel : filter.getCategory() + activate filterModel + filterModel --> ListExpenseCommand : category + deactivate filterModel + ListExpenseCommand -> Model: updateCategoryFilter(category) + activate Model + deactivate Model +else else + ListExpenseCommand -> Model: updateCategoryFilter(null) + activate Model + deactivate Model +end + +alt timespan predicate \nis present + ListExpenseCommand -> filterModel : filter.getTimespan() + activate filterModel + filterModel -> ListExpenseCommand : timespan + deactivate filterModel + ListExpenseCommand -> Model: updateTimespanFilter(timespan) + activate Model + deactivate Model +else else + ListExpenseCommand -> Model: updateTimespanFilter(timespan) + activate Model + deactivate Model +end + +opt category predicate \nis present + ListExpenseCommand -> ListExpenseCommand : combinedPredicate.and(categoryModel.get()) + ListExpenseCommand -[hidden]-> Model +end + +opt timespan \nis present + ListExpenseCommand -> ListExpenseCommand : combinedPredicate.and(timespanPredicate.get()) + ListExpenseCommand -[hidden]-> Model +end + + +ListExpenseCommand -> Model : updateFilteredExpenseList(combinedPredicate) +activate Model +deactivate Model +create CommandResult +ListExpenseCommand -> CommandResult + +activate CommandResult +CommandResult --> ListExpenseCommand +deactivate CommandResult +[<--ListExpenseCommand : result +deactivate ListExpenseCommand +destroy ListExpenseCommand +hide footbox +skinparam MinClassWidth 50 +skinparam ParticipantPadding 10 +skinparam Shadowing false +skinparam DefaultTextAlignment center +skinparam packageStyle Rectangle + + +@enduml diff --git a/docs/diagrams/uml/sequence_diagrams/list_recurring_expense_command.puml b/docs/diagrams/uml/sequence_diagrams/list_recurring_expense_command.puml new file mode 100644 index 00000000000..40753f245f9 --- /dev/null +++ b/docs/diagrams/uml/sequence_diagrams/list_recurring_expense_command.puml @@ -0,0 +1,42 @@ +@startuml ListRecurringExpensesSequenceDiagram +!include ../../style.puml + +hide footbox +skinparam MinClassWidth 50 +skinparam ParticipantPadding 10 +skinparam Shadowing false +skinparam DefaultTextAlignment center +skinparam packageStyle Rectangle + +box Logic LOGIC_COLOR_T1 +participant ":ListRecurringExpenseCommand" as ListRecurringExpenseCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant "model:Model" as Model order 3 MODEL_COLOR +end box + +[-> ListRecurringExpenseCommand : execute(model) +activate ListRecurringExpenseCommand +ListRecurringExpenseCommand -> Model : updateFilteredExpenseList(predicate) +activate Model +deactivate Model +create CommandResult +ListRecurringExpenseCommand -> CommandResult + +activate CommandResult +CommandResult --> ListRecurringExpenseCommand +deactivate CommandResult +[<--ListRecurringExpenseCommand : result +deactivate ListRecurringExpenseCommand +destroy ListRecurringExpenseCommand +hide footbox +skinparam MinClassWidth 50 +skinparam ParticipantPadding 10 +skinparam Shadowing false +skinparam DefaultTextAlignment center +skinparam packageStyle Rectangle + + +@enduml diff --git a/docs/diagrams/uml/storage.puml b/docs/diagrams/uml/storage.puml new file mode 100644 index 00000000000..bdda251db16 --- /dev/null +++ b/docs/diagrams/uml/storage.puml @@ -0,0 +1,109 @@ +@startuml storage + +interface UserPrefsStorage { + getUserPrefsFilePath(): Path + readUserPrefs(): Optional + saveUserPrefs(userPrefs: ReadOnlyUserPrefs): void +} + +interface ExpenseTrackerStorage { + getExpenseTrackerFilePath(): Path + readExpenseTracker(): Optional + readExpenseTracker(filePath: Path): Optional + saveExpenseTracker(expenseTracker: ReadOnlyExpenseTracker): void + saveExpenseTracker(expenseTracker: ReadOnlyExpenseTracker, filePath: Path): void +} + +interface Storage extends ExpenseTrackerStorage, UserPrefsStorage { +} + +class JsonAdaptedBudget { + + JsonAdaptedBudget(source: Budget) + + JsonAdaptedBudget(amount: String) + + toModelType(): Budget + {static} MISSING_FIELD_MESSAGE_FORMAT: String + - amount: String +} + +class JsonAdaptedCategory { + + JsonAdaptedCategory(source: Category) + + JsonAdaptedCategory(categoryName: String, summary: String) + + toModelType(): Category + {static} MISSING_FIELD_MESSAGE_FORMAT: String + - categoryName: String + - summary: String +} + +class JsonAdaptedExpense { + + JsonAdaptedExpense(source: Expense) + + JsonAdaptedExpense(name: String, amount: String, date: String, category: String, category: JsonAdaptedCategory) + + toModelType(): Expense + {static} MISSING_FIELD_MESSAGE_FORMAT: String + - name: String + - amount: String + - date: String + - category: JsonAdaptedCategory +} + +class JsonAdaptedRecurringExpenseManager { + + JsonAdaptedRecurringExpenseManager(source: RecurringExpenseManager) + + JsonAdaptedRecurringExpenseManager(expenseName: String, expenseAmount: String, expenseCategory: JsonAdaptedCategory, nextExpenseDate: String, startDate: String, endDate: String, recurringExpenseType: String) + + toModelType(): RecurringExpenseManager + {static} MISSING_FIELD_MESSAGE_FORMAT: String + - expenseName: String + - expenseAmount: String + - expenseCategory: JsonAdaptedCategory + - nextExpenseDate: String + - startDate: String + - endDate: String + - recurringExpenseType: String +} + +class JsonExpenseTrackerStorage { + + JsonExpenseTrackerStorage(filePath: Path) + + getExpenseTrackerFilePath(): Path + + readExpenseTracker(filePath: Path): Optional + + saveExpenseTracker(expenseTracker: ReadOnlyExpenseTracker, filePath: Path): void + - {static} logger: Logger + - filePath: Path +} + +class JsonSerializableExpenseTracker { + + JsonSerializableExpenseTracker(source: ReadOnlyExpenseTracker) + + JsonSerializableExpenseTracker(listOfCategories: List, listOfExpenses: List, budget: JsonAdaptedBudget, recurringGenerators: List) + + toModelType(): ExpenseTracker + - getAssociatedCategory(expense: Expense, expenseTracker: ExpenseTracker): Category + - getAssociatedCategoryForRecurring(recur: RecurringExpenseManager, expenseTracker: ExpenseTracker): Category + - categories: ArrayList + - expenses: ArrayList + - budget: JsonAdaptedBudget + - recurringGenerators: ArrayList +} + +class JsonUserPrefsStorage { + + JsonUserPrefsStorage(filePath: Path) + + readUserPrefs(prefsFilePath: Path): Optional + - filePath: Path +} + +class StorageManager { + + StorageManager(expenseTracker: ExpenseTrackerStorage, UserPrefsStorage: UserPrefsStorage) + - {static} logger: Logger + - expenseTrackerStorage: ExpenseTrackerStorage + - userPrefsStorage: UserPrefsStorage +} + + +Storage <|-- StorageManager +ExpenseTrackerStorage <|-- JsonExpenseTrackerStorage +UserPrefsStorage <|-- JsonUserPrefsStorage +JsonExpenseTrackerStorage *-- "1" JsonSerializableExpenseTracker +JsonSerializableExpenseTracker *-- "1" JsonAdaptedExpense +JsonSerializableExpenseTracker *-- "1" JsonAdaptedCategory +JsonSerializableExpenseTracker *-- "1" JsonAdaptedBudget +JsonSerializableExpenseTracker *-- "1" JsonAdaptedRecurringExpenseManager +' JsonAdaptedExpense *-- "1" Expense +' JsonAdaptedCategory *-- "1" Category +' JsonAdaptedBudget *-- "1" Budget +' JsonAdaptedRecurringExpenseManager *-- "1" RecurringExpenseManager +@enduml diff --git a/docs/diagrams/uml/ui.puml b/docs/diagrams/uml/ui.puml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/docs/images/ArchitectureDiagram.png b/docs/images/ArchitectureDiagram.png index 86c60246ccb..74bdb501671 100644 Binary files a/docs/images/ArchitectureDiagram.png and b/docs/images/ArchitectureDiagram.png differ diff --git a/docs/images/ArchitectureSequenceDiagram.png b/docs/images/ArchitectureSequenceDiagram.png index 2f1346869d0..c1ed26b45ea 100644 Binary files a/docs/images/ArchitectureSequenceDiagram.png and b/docs/images/ArchitectureSequenceDiagram.png differ diff --git a/docs/images/ComponentManagers.png b/docs/images/ComponentManagers.png index b5764ff9273..b345322362b 100644 Binary files a/docs/images/ComponentManagers.png and b/docs/images/ComponentManagers.png differ diff --git a/docs/images/DeleteSequenceDiagram.png b/docs/images/DeleteSequenceDiagram.png index fa327b39618..aedc85399b3 100644 Binary files a/docs/images/DeleteSequenceDiagram.png and b/docs/images/DeleteSequenceDiagram.png differ diff --git a/docs/images/LogicClassDiagram.png b/docs/images/LogicClassDiagram.png index 9e9ba9f79e5..1371203e3bd 100644 Binary files a/docs/images/LogicClassDiagram.png and b/docs/images/LogicClassDiagram.png differ diff --git a/docs/images/ModelClassDiagram.png b/docs/images/ModelClassDiagram.png index 04070af60d8..b40862a292a 100644 Binary files a/docs/images/ModelClassDiagram.png and b/docs/images/ModelClassDiagram.png differ diff --git a/docs/images/ParserClasses.png b/docs/images/ParserClasses.png index e7b4c8880cd..17b35e322f5 100644 Binary files a/docs/images/ParserClasses.png and b/docs/images/ParserClasses.png differ diff --git a/docs/images/RecurringExpenseStartUpSequenceDiagram.png b/docs/images/RecurringExpenseStartUpSequenceDiagram.png new file mode 100644 index 00000000000..570d8baf4ec Binary files /dev/null and b/docs/images/RecurringExpenseStartUpSequenceDiagram.png differ diff --git a/docs/images/SetBudgetSequenceDiagram.png b/docs/images/SetBudgetSequenceDiagram.png new file mode 100644 index 00000000000..7d30a7a1ad4 Binary files /dev/null and b/docs/images/SetBudgetSequenceDiagram.png differ diff --git a/docs/images/StorageClassDiagram.png b/docs/images/StorageClassDiagram.png index 2533a5c1af0..de673b8ae5e 100644 Binary files a/docs/images/StorageClassDiagram.png and b/docs/images/StorageClassDiagram.png differ diff --git a/docs/images/StorageUmlDiagram.png b/docs/images/StorageUmlDiagram.png new file mode 100644 index 00000000000..fb531a8ea37 Binary files /dev/null and b/docs/images/StorageUmlDiagram.png differ diff --git a/docs/images/Ui.png b/docs/images/Ui.png index 5bd77847aa2..172be4f38d3 100644 Binary files a/docs/images/Ui.png and b/docs/images/Ui.png differ diff --git a/docs/images/UiClassDiagram.png b/docs/images/UiClassDiagram.png index 785e04dbab4..52527f26cd4 100644 Binary files a/docs/images/UiClassDiagram.png and b/docs/images/UiClassDiagram.png differ diff --git a/docs/images/UiStartup.PNG b/docs/images/UiStartup.PNG new file mode 100644 index 00000000000..9baaa67eb9d Binary files /dev/null and b/docs/images/UiStartup.PNG differ diff --git a/docs/images/Ui_Revised.png b/docs/images/Ui_Revised.png new file mode 100644 index 00000000000..2a03fa4db10 Binary files /dev/null and b/docs/images/Ui_Revised.png differ diff --git a/docs/images/activity_diagrams/addRecurringExpenseActivityDiagram.png b/docs/images/activity_diagrams/addRecurringExpenseActivityDiagram.png new file mode 100644 index 00000000000..b3ccdb00438 Binary files /dev/null and b/docs/images/activity_diagrams/addRecurringExpenseActivityDiagram.png differ diff --git a/docs/images/activity_diagrams/deleteRecurringExpenseActivityDiagram.png b/docs/images/activity_diagrams/deleteRecurringExpenseActivityDiagram.png new file mode 100644 index 00000000000..b66c5b1537b Binary files /dev/null and b/docs/images/activity_diagrams/deleteRecurringExpenseActivityDiagram.png differ diff --git a/docs/images/activity_diagrams/editRecurringExpenseActivityDiagram.png b/docs/images/activity_diagrams/editRecurringExpenseActivityDiagram.png new file mode 100644 index 00000000000..8265b1262b4 Binary files /dev/null and b/docs/images/activity_diagrams/editRecurringExpenseActivityDiagram.png differ diff --git a/docs/images/demo/category/addcat.png b/docs/images/demo/category/addcat.png new file mode 100644 index 00000000000..c9562726e8c Binary files /dev/null and b/docs/images/demo/category/addcat.png differ diff --git a/docs/images/demo/category/delcat.png b/docs/images/demo/category/delcat.png new file mode 100644 index 00000000000..adb309657fd Binary files /dev/null and b/docs/images/demo/category/delcat.png differ diff --git a/docs/images/demo/category/edcat1.png b/docs/images/demo/category/edcat1.png new file mode 100644 index 00000000000..2d0e8dbb38a Binary files /dev/null and b/docs/images/demo/category/edcat1.png differ diff --git a/docs/images/demo/category/edcat2.png b/docs/images/demo/category/edcat2.png new file mode 100644 index 00000000000..3b6eb883a7a Binary files /dev/null and b/docs/images/demo/category/edcat2.png differ diff --git a/docs/images/demo/category/lcat.png b/docs/images/demo/category/lcat.png new file mode 100644 index 00000000000..43c997d6ce2 Binary files /dev/null and b/docs/images/demo/category/lcat.png differ diff --git a/docs/images/demo/category/sumcat.png b/docs/images/demo/category/sumcat.png new file mode 100644 index 00000000000..9a760d9df76 Binary files /dev/null and b/docs/images/demo/category/sumcat.png differ diff --git a/docs/images/demo/expense/add1.png b/docs/images/demo/expense/add1.png new file mode 100644 index 00000000000..d8e27a51b46 Binary files /dev/null and b/docs/images/demo/expense/add1.png differ diff --git a/docs/images/demo/expense/add2.png b/docs/images/demo/expense/add2.png new file mode 100644 index 00000000000..cf95388b1f2 Binary files /dev/null and b/docs/images/demo/expense/add2.png differ diff --git a/docs/images/demo/expense/delete.png b/docs/images/demo/expense/delete.png new file mode 100644 index 00000000000..895539c676c Binary files /dev/null and b/docs/images/demo/expense/delete.png differ diff --git a/docs/images/demo/expense/edexp1.png b/docs/images/demo/expense/edexp1.png new file mode 100644 index 00000000000..dfd8de84fa8 Binary files /dev/null and b/docs/images/demo/expense/edexp1.png differ diff --git a/docs/images/demo/expense/edexp2.png b/docs/images/demo/expense/edexp2.png new file mode 100644 index 00000000000..a73bc45f712 Binary files /dev/null and b/docs/images/demo/expense/edexp2.png differ diff --git a/docs/images/demo/expense/find.png b/docs/images/demo/expense/find.png new file mode 100644 index 00000000000..b3afb64e7ac Binary files /dev/null and b/docs/images/demo/expense/find.png differ diff --git a/docs/images/demo/expense/list1.png b/docs/images/demo/expense/list1.png new file mode 100644 index 00000000000..e663ab2e417 Binary files /dev/null and b/docs/images/demo/expense/list1.png differ diff --git a/docs/images/demo/expense/list2.png b/docs/images/demo/expense/list2.png new file mode 100644 index 00000000000..1d61355a614 Binary files /dev/null and b/docs/images/demo/expense/list2.png differ diff --git a/docs/images/demo/expense/list3.png b/docs/images/demo/expense/list3.png new file mode 100644 index 00000000000..1d974a55836 Binary files /dev/null and b/docs/images/demo/expense/list3.png differ diff --git a/docs/images/demo/general/autocomplete_a1.png b/docs/images/demo/general/autocomplete_a1.png new file mode 100644 index 00000000000..8146e388ea6 Binary files /dev/null and b/docs/images/demo/general/autocomplete_a1.png differ diff --git a/docs/images/demo/general/autocomplete_a2.png b/docs/images/demo/general/autocomplete_a2.png new file mode 100644 index 00000000000..18f21fd585f Binary files /dev/null and b/docs/images/demo/general/autocomplete_a2.png differ diff --git a/docs/images/demo/general/autocomplete_b1.png b/docs/images/demo/general/autocomplete_b1.png new file mode 100644 index 00000000000..fc5613f2495 Binary files /dev/null and b/docs/images/demo/general/autocomplete_b1.png differ diff --git a/docs/images/demo/general/autocomplete_b2.png b/docs/images/demo/general/autocomplete_b2.png new file mode 100644 index 00000000000..7770f331a9f Binary files /dev/null and b/docs/images/demo/general/autocomplete_b2.png differ diff --git a/docs/images/demo/general/clear.png b/docs/images/demo/general/clear.png new file mode 100644 index 00000000000..23ba9d2bd4e Binary files /dev/null and b/docs/images/demo/general/clear.png differ diff --git a/docs/images/demo/general/set.png b/docs/images/demo/general/set.png new file mode 100644 index 00000000000..f60069750b5 Binary files /dev/null and b/docs/images/demo/general/set.png differ diff --git a/docs/images/demo/general/summary.png b/docs/images/demo/general/summary.png new file mode 100644 index 00000000000..d3d3cd6e27e Binary files /dev/null and b/docs/images/demo/general/summary.png differ diff --git a/docs/images/demo/intro/fasttrack_labeled_1.png b/docs/images/demo/intro/fasttrack_labeled_1.png new file mode 100644 index 00000000000..bfe5d50ccbd Binary files /dev/null and b/docs/images/demo/intro/fasttrack_labeled_1.png differ diff --git a/docs/images/demo/intro/fasttrack_labeled_2.png b/docs/images/demo/intro/fasttrack_labeled_2.png new file mode 100644 index 00000000000..703cd6b6506 Binary files /dev/null and b/docs/images/demo/intro/fasttrack_labeled_2.png differ diff --git a/docs/images/demo/intro/fasttrack_labeled_3.png b/docs/images/demo/intro/fasttrack_labeled_3.png new file mode 100644 index 00000000000..282410c126a Binary files /dev/null and b/docs/images/demo/intro/fasttrack_labeled_3.png differ diff --git a/docs/images/demo/recurring_expense/addrec1.png b/docs/images/demo/recurring_expense/addrec1.png new file mode 100644 index 00000000000..32adbbf66e3 Binary files /dev/null and b/docs/images/demo/recurring_expense/addrec1.png differ diff --git a/docs/images/demo/recurring_expense/addrec2.png b/docs/images/demo/recurring_expense/addrec2.png new file mode 100644 index 00000000000..045aee9d392 Binary files /dev/null and b/docs/images/demo/recurring_expense/addrec2.png differ diff --git a/docs/images/demo/recurring_expense/delrec.png b/docs/images/demo/recurring_expense/delrec.png new file mode 100644 index 00000000000..c4b6c997326 Binary files /dev/null and b/docs/images/demo/recurring_expense/delrec.png differ diff --git a/docs/images/demo/recurring_expense/edrec1.png b/docs/images/demo/recurring_expense/edrec1.png new file mode 100644 index 00000000000..8f6a8dd21cc Binary files /dev/null and b/docs/images/demo/recurring_expense/edrec1.png differ diff --git a/docs/images/demo/recurring_expense/edrec2.png b/docs/images/demo/recurring_expense/edrec2.png new file mode 100644 index 00000000000..7235cceb755 Binary files /dev/null and b/docs/images/demo/recurring_expense/edrec2.png differ diff --git a/docs/images/demo/recurring_expense/lrec.png b/docs/images/demo/recurring_expense/lrec.png new file mode 100644 index 00000000000..89df5736cc2 Binary files /dev/null and b/docs/images/demo/recurring_expense/lrec.png differ diff --git a/docs/images/fasttrack_logo.png b/docs/images/fasttrack_logo.png new file mode 100644 index 00000000000..5b1d7175f8d Binary files /dev/null and b/docs/images/fasttrack_logo.png differ diff --git a/docs/images/gitsac.png b/docs/images/gitsac.png new file mode 100644 index 00000000000..aabeaaf154c Binary files /dev/null and b/docs/images/gitsac.png differ diff --git a/docs/images/helpMessage.png b/docs/images/helpMessage.png index b1f70470137..98fa6bb1064 100644 Binary files a/docs/images/helpMessage.png and b/docs/images/helpMessage.png differ diff --git a/docs/images/jinbesan.png b/docs/images/jinbesan.png new file mode 100644 index 00000000000..218248ea00a Binary files /dev/null and b/docs/images/jinbesan.png differ diff --git a/docs/images/nicleejy.png b/docs/images/nicleejy.png new file mode 100644 index 00000000000..c910c7c1f64 Binary files /dev/null and b/docs/images/nicleejy.png differ diff --git a/docs/images/randallnhr.png b/docs/images/randallnhr.png new file mode 100644 index 00000000000..c0a7802e2aa Binary files /dev/null and b/docs/images/randallnhr.png differ diff --git a/docs/images/sequence_diagrams/AddExpenseSequenceDiagram.png b/docs/images/sequence_diagrams/AddExpenseSequenceDiagram.png new file mode 100644 index 00000000000..90300281ae5 Binary files /dev/null and b/docs/images/sequence_diagrams/AddExpenseSequenceDiagram.png differ diff --git a/docs/images/sequence_diagrams/DeleteExpenseSequenceDiagram.png b/docs/images/sequence_diagrams/DeleteExpenseSequenceDiagram.png new file mode 100644 index 00000000000..5dd1b61c4fb Binary files /dev/null and b/docs/images/sequence_diagrams/DeleteExpenseSequenceDiagram.png differ diff --git a/docs/images/sequence_diagrams/EditExpenseFindPartSequenceDiagram.png b/docs/images/sequence_diagrams/EditExpenseFindPartSequenceDiagram.png new file mode 100644 index 00000000000..68106e3c8cb Binary files /dev/null and b/docs/images/sequence_diagrams/EditExpenseFindPartSequenceDiagram.png differ diff --git a/docs/images/sequence_diagrams/EditExpenseSequenceDiagram.png b/docs/images/sequence_diagrams/EditExpenseSequenceDiagram.png new file mode 100644 index 00000000000..25e22824399 Binary files /dev/null and b/docs/images/sequence_diagrams/EditExpenseSequenceDiagram.png differ diff --git a/docs/images/sequence_diagrams/FindSequenceDiagram.png b/docs/images/sequence_diagrams/FindSequenceDiagram.png new file mode 100644 index 00000000000..f6645bf079d Binary files /dev/null and b/docs/images/sequence_diagrams/FindSequenceDiagram.png differ diff --git a/docs/images/sequence_diagrams/ListCategorySequenceDiagram.png b/docs/images/sequence_diagrams/ListCategorySequenceDiagram.png new file mode 100644 index 00000000000..92c3622873f Binary files /dev/null and b/docs/images/sequence_diagrams/ListCategorySequenceDiagram.png differ diff --git a/docs/images/sequence_diagrams/ListExpensesSequenceDiagram.png b/docs/images/sequence_diagrams/ListExpensesSequenceDiagram.png new file mode 100644 index 00000000000..48bba05c6e8 Binary files /dev/null and b/docs/images/sequence_diagrams/ListExpensesSequenceDiagram.png differ diff --git a/docs/images/sequence_diagrams/ListRecurringExpensesSequenceDiagram.png b/docs/images/sequence_diagrams/ListRecurringExpensesSequenceDiagram.png new file mode 100644 index 00000000000..514eb00b6b7 Binary files /dev/null and b/docs/images/sequence_diagrams/ListRecurringExpensesSequenceDiagram.png differ diff --git a/docs/images/sequence_diagrams/RecurringExpenseStartUpSequenceDiagram.png b/docs/images/sequence_diagrams/RecurringExpenseStartUpSequenceDiagram.png new file mode 100644 index 00000000000..85888d90559 Binary files /dev/null and b/docs/images/sequence_diagrams/RecurringExpenseStartUpSequenceDiagram.png differ diff --git a/docs/images/sequence_diagrams/SetBudgetSequenceDiagram.png b/docs/images/sequence_diagrams/SetBudgetSequenceDiagram.png new file mode 100644 index 00000000000..91685941870 Binary files /dev/null and b/docs/images/sequence_diagrams/SetBudgetSequenceDiagram.png differ diff --git a/docs/images/shirsho-12.png b/docs/images/shirsho-12.png new file mode 100644 index 00000000000..d0c0f4b567f Binary files /dev/null and b/docs/images/shirsho-12.png differ diff --git a/docs/images/uml_diagrams/commands/add_commands.png b/docs/images/uml_diagrams/commands/add_commands.png new file mode 100644 index 00000000000..4ada1a3f94e Binary files /dev/null and b/docs/images/uml_diagrams/commands/add_commands.png differ diff --git a/docs/images/uml_diagrams/commands/commands.png b/docs/images/uml_diagrams/commands/commands.png new file mode 100644 index 00000000000..e4c48d96789 Binary files /dev/null and b/docs/images/uml_diagrams/commands/commands.png differ diff --git a/docs/images/uml_diagrams/commands/delete_commands.png b/docs/images/uml_diagrams/commands/delete_commands.png new file mode 100644 index 00000000000..ac28a747d37 Binary files /dev/null and b/docs/images/uml_diagrams/commands/delete_commands.png differ diff --git a/docs/images/uml_diagrams/commands/edit_commands.png b/docs/images/uml_diagrams/commands/edit_commands.png new file mode 100644 index 00000000000..0b91081d0dc Binary files /dev/null and b/docs/images/uml_diagrams/commands/edit_commands.png differ diff --git a/docs/images/uml_diagrams/commands/general_commands.png b/docs/images/uml_diagrams/commands/general_commands.png new file mode 100644 index 00000000000..87d7c356f39 Binary files /dev/null and b/docs/images/uml_diagrams/commands/general_commands.png differ diff --git a/docs/images/uml_diagrams/commands/list_commands.png b/docs/images/uml_diagrams/commands/list_commands.png new file mode 100644 index 00000000000..5a8b5dd30b1 Binary files /dev/null and b/docs/images/uml_diagrams/commands/list_commands.png differ diff --git a/docs/index.md b/docs/index.md index 7601dbaad0d..c29112653a4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,17 +1,17 @@ --- layout: page -title: AddressBook Level-3 +title: FastTrack --- -[![CI Status](https://github.com/se-edu/addressbook-level3/workflows/Java%20CI/badge.svg)](https://github.com/se-edu/addressbook-level3/actions) -[![codecov](https://codecov.io/gh/se-edu/addressbook-level3/branch/master/graph/badge.svg)](https://codecov.io/gh/se-edu/addressbook-level3) +[![CI Status](https://github.com/AY2223S2-CS2103T-W09-2/tp/workflows/Java%20CI/badge.svg)](https://github.com/AY2223S2-CS2103T-W09-2/tp/actions) +[![codecov](https://codecov.io/gh/AY2223S2-CS2103T-W09-2/tp/branch/master/graph/badge.svg)](https://codecov.io/gh/AY2223S2-CS2103T-W09-2/tp/) ![Ui](images/Ui.png) -**AddressBook is a desktop application for managing your contact details.** While it has a GUI, most of the user interactions happen using a CLI (Command Line Interface). +**FastTrack is the answer to your expense management prayers.** It is a desktop application with a GUI. Most of the user interactions, however, happen using a CLI (Command Line Interface). -* If you are interested in using AddressBook, head over to the [_Quick Start_ section of the **User Guide**](UserGuide.html#quick-start). -* If you are interested about developing AddressBook, the [**Developer Guide**](DeveloperGuide.html) is a good place to start. +* If you are interested in using FastTrack, head over to the [_Quick Start_ section of the **User Guide**](UserGuide.html#quick-start). +* If you are interested about developing FastTrack, the [**Developer Guide**](DeveloperGuide.html) is a good place to start. **Acknowledgements** diff --git a/docs/team/gitsac.md b/docs/team/gitsac.md new file mode 100644 index 00000000000..c00800de029 --- /dev/null +++ b/docs/team/gitsac.md @@ -0,0 +1,57 @@ +--- +layout: page +title: Isaac's Project Portfolio Page +--- + +### Overview + +FastTrack is a desktop application to help you keep track of daily expenses, optimised for use via a command line interface (CLI). With this app, you can easily add expenses by category, view a summary of what has been spent in total, by category or for the week. The user interface is intuitive and easy-to-use. Overall, FastTrack aims to speed up the time taken to log expenses, saving valuable time for the user. + +### Summary of Contributions + +- Code contributed: [RepoSense link](https://nus-cs2103-ay2223s2.github.io/tp-dashboard/?search=gitsac&breakdown=true&sort=groupTitle%20dsc&sortWithin=title&since=2023-02-17&timeframe=commit&mergegroup=&groupSelect=groupByRepos&checkedFileTypes=docs~functional-code~test-code~other) + +Enhancements implemented: +- Helped with implementation of `Category` class. + + - Defined the `Category` class with its fields. (Pull request [#30](https://github.com/AY2223S2-CS2103T-W09-2/tp/issues/30)) + - Edited storage system to accommodate `Category` class. (Pull request [#29](https://github.com/AY2223S2-CS2103T-W09-2/tp/issues/29)) + + +- Helped with the implementation of `RecurringExpenseManager` class. + + - Edited storage system to accommodate `Category` class. (Pull request [#95](https://github.com/AY2223S2-CS2103T-W09-2/tp/issues/95)) + + +- Implemented `edit` function for all 3 main classes used (`Category`, `Expense` and `RecurringExpenseManager`) + + - Implemented `EditExpenseCommand` along with its necessary helper parser class. (Pull request [#77](https://github.com/AY2223S2-CS2103T-W09-2/tp/issues/79)) + - Implemented `EditCategoryCommand` along with necessary parser class. (Pull request [#78](https://github.com/AY2223S2-CS2103T-W09-2/tp/issues/78)) + - Implemented `EditRecurringExpenseManagerCommand` along with necessary parser class. (Pull request [#130](https://github.com/AY2223S2-CS2103T-W09-2/tp/issues/130)) + + +- Added basic startup data that was adapted for FastTrack's usage. (Morphed from AB3's given sample data) (Pull request [#107](https://github.com/AY2223S2-CS2103T-W09-2/tp/issues/107)) + + +- Added functionality that causes list of `Expense` in FastTrack to be sorted by date upon any operations (adding/deleting expenses) (Pull request [#136](https://github.com/AY2223S2-CS2103T-W09-2/tp/issues/136)) + + +Contributions to the UG: +- Wrote about features in initial draft and added tables denoting the parameters used as well as simple explanations. + +Contributions to the DG: +- Worked on the Implementations portion of the DG. +- Wrote about the Effort section of the DG. +- Sketched multiple sequence diagrams that were translated through PlantUML to be used in the DG. + +Contributions to team-based tasks: +- Participated in weekly (sometimes biweekly) meetings to discuss project structure and direction. +- Took part actively in debugging other teammate's issues. +- Fixed several bugs reported from PE Dry Run ([#167](https://github.com/AY2223S2-CS2103T-W09-2/tp/issues/167), [#170](https://github.com/AY2223S2-CS2103T-W09-2/tp/issues/170), [#179](https://github.com/AY2223S2-CS2103T-W09-2/tp/issues/179)). +- Helped to regulate pull requests from team-mates and merged them only when they passed CI and internal test cases. + +Review/Mentoring Contributions: +- Reviewed multiple PRs made by teammates ([#101](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/101), [#90](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/90), [#72](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/72)). + +Contributions beyond team project: +- Reported bugs for another team during the PE-Dry run (T11-4): HospiSearch. diff --git a/docs/team/jinbesan.md b/docs/team/jinbesan.md new file mode 100644 index 00000000000..1f9e92e1aba --- /dev/null +++ b/docs/team/jinbesan.md @@ -0,0 +1,50 @@ +--- +layout: page +title: Wen Hong's Project Portfolio Page +--- + +### Overview + +FastTrack is a desktop application to help you keep track of daily expenses, optimised for use via a command line interface (CLI). With this app, you can easily add expenses by category, view a summary of what has been spent in total, by category or for the week. The user interface is intuitive and easy-to-use. Overall, FastTrack aims to speed up the time taken to log expenses, saving valuable time for the user. + +### Summary of Contributions + +- Code contributed: [RepoSense link](https://nus-cs2103-ay2223s2.github.io/tp-dashboard/?search=jinbesan&breakdown=true&sort=groupTitle%20dsc&sortWithin=title&since=2023-02-17&timeframe=commit&mergegroup=&groupSelect=groupByRepos&checkedFileTypes=docs~functional-code~test-code~other) + +Enhancements implemented: + +- Implementation of `find` Command (Pull request [#90](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/90)) + - Addition of `ExpenseContainsKeywordsPredicate` +- Implementation of `list` Command + - Addition of `ExpenseInCategoryPredicate` (Pull request [#101](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/101)) + - Addition of `ExpenseInTimespanPredicate` (Pull request [#104](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/104)) +- Implementation of `lrec` Command (Pull request [#212](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/212)) + +- Updated `help` window (Pull request [#66](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/66)) +- Added TestUtils (Pull request [#86](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/86)) + +- Helped with implementation of `ParserUtil` class. + +Contributions to the UG: +- Wrote about features in initial draft and added tables denoting the parameters used as well as simple explanations. (Pull request [#126](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/126)) +- Wrote Introduction, Why to use FastTrack, Purpose of Guide, Understanding Guide, Quick Start, GUI Walkthrough sections of the User Guide (Pull request [#212](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/212)) +- Rearranged structure of User Guide to be more user-friendly (Pull request [#236](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/236)) +- Reformatting of tables for commands and tips + +Contributions to the DG: +- Wrote about features in initial draft and added tables denoting the parameters used as well as some use cases. (Pull request [#105](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/105), Pull request [#127](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/127)) +- Sketched multiple sequence diagrams that were translated through PlantUML to be used in the DG. + +Contributions to team-based tasks: +- Participated in weekly (sometimes biweekly) meetings to discuss project structure and direction. +- Proposed addition of Recurring Expenses feature to create selling point of app +- Participated in discussion of project architecture +- Suggested structure of implementation of Recurring Expenses. +- Took part actively in debugging other teammate's issues. +- Helped to regulate pull requests from team-mates and merged them only when they passed CI and internal test cases. + +Review/Mentoring Contributions: +- Reviewed multiple PRs made by teammates ([#146](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/146), [#89](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/89), [#145](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/145)). + +Contributions beyond team project: +- Reported bugs for another team during the PE-Dry run (T15-3): Vimification. diff --git a/docs/team/johndoe.md b/docs/team/johndoe.md deleted file mode 100644 index 773a07794e2..00000000000 --- a/docs/team/johndoe.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -layout: page -title: John Doe's Project Portfolio Page ---- - -### Project: AddressBook Level 3 - -AddressBook - Level 3 is a desktop address book application used for teaching Software Engineering principles. The user interacts with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 10 kLoC. - -Given below are my contributions to the project. - -* **New Feature**: Added the ability to undo/redo previous commands. - * What it does: allows the user to undo all previous commands one at a time. Preceding undo commands can be reversed by using the redo command. - * Justification: This feature improves the product significantly because a user can make mistakes in commands and the app should provide a convenient way to rectify them. - * Highlights: This enhancement affects existing commands and commands to be added in future. It required an in-depth analysis of design alternatives. The implementation too was challenging as it required changes to existing commands. - * Credits: *{mention here if you reused any code/ideas from elsewhere or if a third-party library is heavily used in the feature so that a reader can make a more accurate judgement of how much effort went into the feature}* - -* **New Feature**: Added a history command that allows the user to navigate to previous commands using up/down keys. - -* **Code contributed**: [RepoSense link]() - -* **Project management**: - * Managed releases `v1.3` - `v1.5rc` (3 releases) on GitHub - -* **Enhancements to existing features**: - * Updated the GUI color scheme (Pull requests [\#33](), [\#34]()) - * Wrote additional tests for existing features to increase coverage from 88% to 92% (Pull requests [\#36](), [\#38]()) - -* **Documentation**: - * User Guide: - * Added documentation for the features `delete` and `find` [\#72]() - * Did cosmetic tweaks to existing documentation of features `clear`, `exit`: [\#74]() - * Developer Guide: - * Added implementation details of the `delete` feature. - -* **Community**: - * PRs reviewed (with non-trivial review comments): [\#12](), [\#32](), [\#19](), [\#42]() - * Contributed to forum discussions (examples: [1](), [2](), [3](), [4]()) - * Reported bugs and suggestions for other teams in the class (examples: [1](), [2](), [3]()) - * Some parts of the history feature I added was adopted by several other class mates ([1](), [2]()) - -* **Tools**: - * Integrated a third party library (Natty) to the project ([\#42]()) - * Integrated a new Github plugin (CircleCI) to the team repo - -* _{you can add/remove categories in the list above}_ diff --git a/docs/team/nicleejy.md b/docs/team/nicleejy.md new file mode 100644 index 00000000000..aa0b68db4fd --- /dev/null +++ b/docs/team/nicleejy.md @@ -0,0 +1,47 @@ +--- +layout: page +title: Nicholas's Project Portfolio Page +--- +### Overview + +FastTrack is a desktop application to help you keep track of daily expenses, optimised for use via a command line interface (CLI). With this app, you can easily add expenses by category, view a summary of what has been spent in total, by category or for the week. The user interface is intuitive and easy-to-use. Overall, FastTrack aims to speed up the time taken to log expenses, saving valuable time for the user. + +### Summary of Contributions + +- Code contributed: [RepoSense link](https://nus-cs2103-ay2223s2.github.io/tp-dashboard/?search=nicleejy&breakdown=true&sort=groupTitle%20dsc&sortWithin=title&since=2023-02-17&timeframe=commit&mergegroup=&groupSelect=groupByRepos&checkedFileTypes=docs~functional-code~test-code~other) + + +Enhancements implemented: +- Implemented new category autocomplete feature which allows users to autocomplete category names from a list of suggestions using arrow/enter/tab keys (Pull request [#148](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/148)) +- Added new UI screen for recurring expense feature (Pull request [#145](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/145)) +- Implemented the expense summary statistics feature (Pull request [#111](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/111)) + - Added new UI section for summary statistics data + - Implemented new `AnalyticModelManager` class to manage expense data state + - Utilised Observer Pattern to integrate expense statistics data into `StatisticsPanel` UI component, ensuring statistics are updated in real time +- Implemented add expense feature (Pull request [#72](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/72)) + - Implemented `AddExpenseCommand` which encapsulates the command request details + - Added `ExpenseCommandParser` containing various parser methods to parse dates and prices to interpret the command +- Added sample data for recurring expenses (Morphed from AB3's given sample data) (Pull request [#224](https://github.com/AY2223S2-CS2103T-W09-2/tp/issues/224)) + +Contributions to the UG: +- Wrote introduction section for the initial draft of the UG (Pull request [#124](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/124)) +- Updated second draft of UG (Pull request [#213](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/213)) + - Update all feature sections + - added FAQ section + - Added annotated diagrams for each command and GUI walkthrough + +Contributions to the DG: +- Add expense summary feature and implementation details to DG (Pull request [#110](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/110)) +- Sketched and implemented Activity Diagrams in PlantUML for recurring expense (Pull request [#218](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/218)) +- Add writeup for autocompletion feature in the DG (Pull request [#218](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/218)) + +Contributions to team-based tasks: +- Participated in weekly (sometimes biweekly) meetings to discuss project structure and direction. +- Took part actively in debugging other teammate's issues. +- Fixed [several bugs](https://github.com/AY2223S2-CS2103T-W09-2/tp/issues?q=is%3Aissue+is%3Aclosed+assignee%3Anicleejy+pe-d) reported from PE Dry Run + +Review/Mentoring Contributions: +- Reviewed [23 PRs](https://github.com/AY2223S2-CS2103T-W09-2/tp/pulls?q=is%3Apr+is%3Aclosed+reviewed-by%3A%40me) made by teammates + +Contributions beyond team project: +- Reported bugs for another team during the PE-Dry run (F10-1): OfficeConnect. diff --git a/docs/team/randallnhr.md b/docs/team/randallnhr.md new file mode 100644 index 00000000000..febdb74e0da --- /dev/null +++ b/docs/team/randallnhr.md @@ -0,0 +1,55 @@ +--- +layout: page +title: Randall's Project Portfolio Page +--- + +### Overview + +FastTrack is a desktop application to help you keep track of daily expenses, optimised for use via a command line interface (CLI). With this app, you can easily add expenses by category, view a summary of what has been spent in total, by category or for the week. The user interface is intuitive and easy-to-use. Overall, FastTrack aims to speed up the time taken to log expenses, saving valuable time for the user. + +### Summary of Contributions + +**Code contributed:** + +The following [link](https://nus-cs2103-ay2223s2.github.io/tp-dashboard/?search=randallnhr&breakdown=true&sort=groupTitle+dsc&sortWithin=title&since=2023-02-17&timeframe=commit&mergegroup=&groupSelect=groupByRepos&checkedFileTypes=docs%7Efunctional-code%7Etest-code%7Eother) is my code contribution. + +#### **Enhancements implemented:** +* Implemented commands for `Category` + * `addcat` - allows users of FastTrack to add a new `Category` into FastTrack. (PR [#68](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/68)) + * Allow users to add category without summary (PR [#118](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/118)) + * `delcat` - allows users to delete an existing `Category` in FastTrack. (PR [#68](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/68)) + * Expenses with the deleted category will have its category replaced with the `MiscellaneuosCategory`. (PR [#109](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/109)) + * `lcat` - allows users to list all added `Category`, used to determine index for edit and delete category commands. (PR [#68](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/68)) + * `sumcat` - allows users to view category summary. (PR [#119](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/119)) +* Implemented `CLEAR` command. (PR [#120](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/120)) + * What it does: Wipes the storage of FastTrack to a clean slate. This is useful when the user first opens FastTrack and wants to delete the sample data. +* Implemented `Budget` class and linked it to the UI to update statistics. (PR [#138](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/138)) + * What it does: Allows users to add a monthly budget into FastTrack. This is used in conjunction with the Statistics feature to allow users to have an easy way to see how much of the budget has been utilised. +* Implemented commands for `RecurringExpenseManager` + * `addrec` - allows users to add `RecurringExpenseManager` objects into FastTrack. (PR [#140](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/140)) + * `delrec` - allows users to delete a `RecurringExpenseManager` object. (PR [#140](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/140)) + +#### **Contributions to the UG:** +* Added command summary for: + * Expense commands + * Category commands + * General commands + +#### **Contributions to the DG:** +* Added several use cases. (PR [#37](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/37)) +* Added purpose of the guide, how to use this guide and acknowledgement. (PR [#209](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/209)) +* Added Recurring Expense implementation. (PR [#222](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/222)) +* Added Budget implementation and linked to Statistics implementation. (PR [#222](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/222)) +* Added writeup for category features: (PR [#222](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/222)) + * Adding a category + * Deleting a category +* Created sequence diagrams for `set` and Recurring Expense feature. (PR [#235](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/235)) +* Created PlantUML diagrams for high level architecture and class diagrams. (PR [#249](https://github.com/AY2223S2-CS2103T-W09-2/tp/pull/249)) +#### **Contributions to team-based tasks:** +* Organised weekly meetings to discuss project structure and direction. +* Took part actively in debugging other teammate's issues. + +#### **Review/Mentoring Contributions:** +* Reviewed several [PRs](https://github.com/AY2223S2-CS2103T-W09-2/tp/pulls?q=is%3Apr+is%3Aclosed+reviewed-by%3A%40me) made by teammates. + +#### **Contributions beyond team project:** diff --git a/docs/team/shirsho-12.md b/docs/team/shirsho-12.md new file mode 100644 index 00000000000..180ef3726f7 --- /dev/null +++ b/docs/team/shirsho-12.md @@ -0,0 +1,72 @@ +--- +layout: page +title: Shirshajit's Project Portfolio Page +--- + +### Project: FastTrack + +FastTrack is an expense tracking app that helps computing students keep track of their expenses by providing a simple and convenient command-line interface. It is optimized for use via a Command Line Interface (CLI) while still having the benefits of a Graphical User Interface (GUI). If you can type fast, FastTrack can get your expense management tasks done faster than traditional GUI apps. + +Given below are my contributions to the project. + +### Summary of Contributions + +- **New Feature**: `Recurring Expenses`: Created the recurring expense functionality + + - What it does: Creates recurring expenses for the user, i.e., expenses that occur at regular intervals. Theese expenses are automatically generated based on the user's input and current date. The user can also view all recurring expenses, delete recurring expenses, and mark recurring expenses as done. + - Justification: This feature improves the product significantly because a large number of users would prefer to track their recurring expenses. This feature also allows the user to save time by not having to manually create recurring expenses. + - Highlights: This enhancement affects existing commands and commands to be added in the future. It required an in-depth analysis of design alternatives. The implementation too was challenging as it required changes to existing commands and the logic to handle the recurring expenses. + +- **New Feature**: `Category` and `Expense` models: Created the main Category and Expense models. + + - What it does: The Category model is used to store the different categories of expenses that the user can add. The Expense model is used to store the different expenses that the user can add. + - Justification: This feature improves the product significantly because it allows the user to add expenses and categorize them. This feature also allows the user to save time by not having to manually create recurring expenses. + - Highlights: This enhancement affects existing commands and commands to be added in the future. It required an in-depth analysis of design alternatives. The implementation was challenging as it required changes to existing commands and the logic to handle the recurring expenses. + +- **Code Contributions**: [RepoSense link](https://nus-cs2103-ay2223s2.github.io/tp-dashboard/?search=shirsho-12&breakdown=true&sort=groupTitle&sortWithin=title&since=2023-02-17&timeframe=commit&mergegroup=&groupSelect=groupByRepos&checkedFileTypes=docs~functional-code~test-code~other) + +- **Project Management**: + - Managed releases `v1.1` - `v1.4` (3 releases) on GitHub + - Managed issue creation and assignment, milestone creation, issue labelling, and issue closing + - Enforced coding standards and code quality + - Managed the project's [issue tracker](https://github.com/AY2223S2-CS2103T-W09-2/tp/issues) + - Managed the project's [pull requests](https://github.com/AY2223S2-CS2103T-W09-2/tp/pulls) + - Managed the project [repository](https://github.com/AY2223S2-CS2103T-W09-2/tp) with the help of Isaac,[@gitsac](https://github.com/gitsac/), and Nicholas, [@niceleejy](https://github.com/niceleejy/). + +**Enhancements implemented**: + +- Implemented the recurring expense generation functionality +- Developed Expense Type enums for the different frequency types of recurring expenses +- Implemented functionality to strip out additional whitespace in user input + +**Contributions to the UG:** + +- Added documentation for the Category and Expense models +- Added documentation for the recurring expense functionality + +**Contributions to the DG:** + +- Created Activity Diagrams for all the commands +- Created UML Diagrams for the Category and Expense models, Commands, and the Parser +- Designed the architecture diagrams for the project + +**Contributions to team-based tasks:** + +- Managed the setting up of the project repository and test suite +- Created test cases for multiple commands, models, and storage +- Reviewed and merged multiple pull requests +- Redesigned the architecture of the Command classes to make it more extensible +- Fixed a number of bugs in the parser and storage classes +- Fixed test cases that were failing due to changes in the codebase +- Refactored the codebase to improve code quality + +**Review/Mentoring Contributions:** + +- Reviewed and provided feedback on multiple pull requests +- Reviewed and provided feedback on multiple issues +- Reviewed and provided feedback on multiple code quality issues + +**Contributions beyond team project:** + +- Reported bugs and suggestions for improvement for other team projects +- Participated in the discussion forum and helped other students with their queries diff --git a/docs/tutorials/AddRemark.md b/docs/tutorials/AddRemark.md index 880c701042f..6c167fa06b3 100644 --- a/docs/tutorials/AddRemark.md +++ b/docs/tutorials/AddRemark.md @@ -16,16 +16,16 @@ We’ll assume that you have already set up the development environment as outli Looking in the `logic.command` package, you will notice that each existing command have their own class. All the commands inherit from the abstract class `Command` which means that they must override `execute()`. Each `Command` returns an instance of `CommandResult` upon success and `CommandResult#feedbackToUser` is printed to the `ResultDisplay`. -Let’s start by creating a new `RemarkCommand` class in the `src/main/java/seedu/address/logic/command` directory. +Let’s start by creating a new `RemarkCommand` class in the `src/main/java/fasttrack/logic/command` directory. For now, let’s keep `RemarkCommand` as simple as possible and print some output. We accomplish that by returning a `CommandResult` with an accompanying message. **`RemarkCommand.java`:** ``` java -package seedu.address.logic.commands; +package fasttrack.logic.commands; -import seedu.address.model.Model; +import fasttrack.model.DataModelodel; /** * Changes the remark of an existing person in the address book. @@ -35,7 +35,7 @@ public class RemarkCommand extends Command { public static final String COMMAND_WORD = "remark"; @Override - public CommandResult execute(Model model) { + public CommandResult execute(Model dataModel) { return new CommandResult("Hello from remark"); } } @@ -77,7 +77,7 @@ Following the convention in other commands, we add relevant messages as constant "Remark command not implemented yet"; @Override - public CommandResult execute(Model model) throws CommandException { + public CommandResult execute(Model dataModel) throws CommandException { throw new CommandException(MESSAGE_NOT_IMPLEMENTED_YET); } ``` @@ -111,7 +111,7 @@ public class RemarkCommand extends Command { this.remark = remark; } @Override - public CommandResult execute(Model model) throws CommandException { + public CommandResult execute(Model dataModel) throws CommandException { throw new CommandException( String.format(MESSAGE_ARGUMENTS, index.getOneBased(), remark)); } @@ -223,13 +223,13 @@ public RemarkCommand parse(String args) throws ParseException { If you are stuck, check out the sample [here](https://github.com/se-edu/addressbook-level3/commit/dc6d5139d08f6403da0ec624ea32bd79a2ae0cbf#diff-8bf239e8e9529369b577701303ddd96af93178b4ed6735f91c2d8488b20c6b4a). -## Add `Remark` to the model +## Add `Remark` to the dataModel -Now that we have all the information that we need, let’s lay the groundwork for propagating the remarks added into the in-memory storage of person data. We achieve that by working with the `Person` model. Each field in a Person is implemented as a separate class (e.g. a `Name` object represents the person’s name). That means we should add a `Remark` class so that we can use a `Remark` object to represent a remark given to a person. +Now that we have all the information that we need, let’s lay the groundwork for propagating the remarks added into the in-memory storage of person data. We achieve that by working with the `Person` dataModel. Each field in a Person is implemented as a separate class (e.g. a `Name` object represents the person’s name). That means we should add a `Remark` class so that we can use a `Remark` object to represent a remark given to a person. ### Add a new `Remark` class -Create a new `Remark` in `seedu.address.model.person`. Since a `Remark` is a field that is similar to `Address`, we can reuse a significant bit of code. +Create a new `Remark` in `seedu.address.dataModel.person`. Since a `Remark` is a field that is similar to `Address`, we can reuse a significant bit of code. A copy-paste and search-replace later, you should have something like [this](https://github.com/se-edu/addressbook-level3/commit/4516e099699baa9e2d51801bd26f016d812dedcc#diff-41bb13c581e280c686198251ad6cc337cd5e27032772f06ed9bf7f1440995ece). Note how `Remark` has no constrains and thus does not require input validation. @@ -295,7 +295,7 @@ While the changes to code may be minimal, the test data will have to be updated
-:exclamation: You must delete AddressBook’s storage file located at `/data/addressbook.json` before running it! Not doing so will cause AddressBook to default to an empty address book! +:exclamation: You must delete AddressBook’s storage file located at `/data/fastTrack.json` before running it! Not doing so will cause AddressBook to default to an empty address book!
@@ -336,8 +336,8 @@ save it with `Model#setPerson()`. public static final String MESSAGE_DELETE_REMARK_SUCCESS = "Removed remark from Person: %1$s"; //... @Override - public CommandResult execute(Model model) throws CommandException { - List lastShownList = model.getFilteredPersonList(); + public CommandResult execute(Model dataModel) throws CommandException { + List lastShownList = dataModel.getFilteredPersonList(); if (index.getZeroBased() >= lastShownList.size()) { throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); @@ -348,8 +348,8 @@ save it with `Model#setPerson()`. personToEdit.getName(), personToEdit.getPhone(), personToEdit.getEmail(), personToEdit.getAddress(), remark, personToEdit.getTags()); - model.setPerson(personToEdit, editedPerson); - model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + dataModel.setPerson(personToEdit, editedPerson); + dataModel.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); return new CommandResult(generateSuccessMessage(editedPerson)); } diff --git a/docs/tutorials/RemovingFields.md b/docs/tutorials/RemovingFields.md index f29169bc924..a59bf96ae0a 100644 --- a/docs/tutorials/RemovingFields.md +++ b/docs/tutorials/RemovingFields.md @@ -28,7 +28,7 @@ IntelliJ IDEA provides a refactoring tool that can identify *most* parts of a re ### Assisted refactoring -The `address` field in `Person` is actually an instance of the `seedu.address.model.person.Address` class. Since removing the `Address` class will break the application, we start by identifying `Address`'s usages. This allows us to see code that depends on `Address` to function properly and edit them on a case-by-case basis. Right-click the `Address` class and select `Refactor` \> `Safe Delete` through the menu. +The `address` field in `Person` is actually an instance of the `seedu.address.dataModel.person.Address` class. Since removing the `Address` class will break the application, we start by identifying `Address`'s usages. This allows us to see code that depends on `Address` to function properly and edit them on a case-by-case basis. Right-click the `Address` class and select `Refactor` \> `Safe Delete` through the menu. * :bulb: To make things simpler, you can unselect the options `Search in comments and strings` and `Search for text occurrences` ![Usages detected](../images/remove/UnsafeDelete.png) diff --git a/docs/tutorials/TracingCode.md b/docs/tutorials/TracingCode.md index 4fb62a83ef6..67079a257e5 100644 --- a/docs/tutorials/TracingCode.md +++ b/docs/tutorials/TracingCode.md @@ -122,12 +122,12 @@ Recall from the User Guide that the `edit` command has the format: `edit INDEX [ //Parse user input from String to a Command Command command = addressBookParser.parseCommand(commandText); //Executes the Command and stores the result - commandResult = command.execute(model); + commandResult = command.execute(dataModel); try { - //We can deduce that the previous line of code modifies model in some way + //We can deduce that the previous line of code modifies dataModel in some way // since it's being stored here. - storage.saveAddressBook(model.getAddressBook()); + storage.saveAddressBook(dataModel.getAddressBook()); } catch (IOException ioe) { throw new CommandException(FILE_OPS_ERROR_MESSAGE + ioe, ioe); } @@ -187,26 +187,26 @@ Recall from the User Guide that the `edit` command has the format: `edit INDEX [ **`EditCommand#execute()`:** ``` java @Override - public CommandResult execute(Model model) throws CommandException { + public CommandResult execute(Model dataModel) throws CommandException { ... Person personToEdit = lastShownList.get(index.getZeroBased()); Person editedPerson = createEditedPerson(personToEdit, editPersonDescriptor); - if (!personToEdit.isSamePerson(editedPerson) && model.hasPerson(editedPerson)) { + if (!personToEdit.isSamePerson(editedPerson) && dataModel.hasPerson(editedPerson)) { throw new CommandException(MESSAGE_DUPLICATE_PERSON); } - model.setPerson(personToEdit, editedPerson); - model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + dataModel.setPerson(personToEdit, editedPerson); + dataModel.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); return new CommandResult(String.format(MESSAGE_EDIT_PERSON_SUCCESS, editedPerson)); } ``` -1. As suspected, `command#execute()` does indeed make changes to the `model` object. Specifically, +1. As suspected, `command#execute()` does indeed make changes to the `dataModel` object. Specifically, * it uses the `setPerson()` method (defined in the interface `Model` and implemented in `ModelManager` as per the usual pattern) to update the person data. * it uses the `updateFilteredPersonList` method to ask the `Model` to populate the 'filtered list' with _all_ persons.
FYI, The 'filtered list' is the list of persons resulting from the most recent operation that will be shown to the user immediately after. For the `edit` command, we populate it with all the persons so that the user can see the edited person along with all other persons. If this was a `find` command, we would be setting that list to contain the search results instead.
To provide some context, given below is the class diagram of the `Model` component. See if you can figure out where the 'filtered list' of persons is being tracked.
- * :bulb: This may be a good time to read through the [`Model` component section of the DG](../DeveloperGuide.html#model-component) + * :bulb: This may be a good time to read through the [`Model` component section of the DG](../DeveloperGuide.html#dataModel-component) 1. As you step through the rest of the statements in the `EditCommand#execute()` method, you'll see that it creates a `CommandResult` object (containing information about the result of the execution) and returns it.
Advancing the debugger by one more step should take you back to the middle of the `LogicManager#execute()` method.
@@ -217,7 +217,7 @@ Recall from the User Guide that the `edit` command has the format: `edit INDEX [ 1. Similar to before, you can step over/into statements in the `LogicManager#execute()` method to examine how the control is transferred to the `Storage` component and what happens inside that component. -
:bulb: **Intellij Tip:** When trying to step into a statement such as `storage.saveAddressBook(model.getAddressBook())` which contains multiple method calls, Intellij will let you choose (by clicking) which one you want to step into. +
:bulb: **Intellij Tip:** When trying to step into a statement such as `storage.saveAddressBook(dataModel.getAddressBook())` which contains multiple method calls, Intellij will let you choose (by clicking) which one you want to step into.
1. As you step through the code inside the `Storage` component, you will eventually arrive at the `JsonAddressBook#saveAddressBook()` method which calls the `JsonSerializableAddressBook` constructor, to create an object that can be _serialized_ (i.e., stored in storage medium) in JSON format. That constructor is given below (with added line breaks for easier readability): diff --git a/fasttrack.log.0 b/fasttrack.log.0 new file mode 100644 index 00000000000..e69de29bb2d diff --git a/fasttrack.log.0.1 b/fasttrack.log.0.1 new file mode 100644 index 00000000000..e0b22d7066e Binary files /dev/null and b/fasttrack.log.0.1 differ diff --git a/preferences.json b/preferences.json new file mode 100644 index 00000000000..1d8ca70fd1d --- /dev/null +++ b/preferences.json @@ -0,0 +1,11 @@ +{ + "guiSettings" : { + "windowWidth" : 1000.0, + "windowHeight" : 700.0, + "windowCoordinates" : { + "x" : 208, + "y" : 25 + } + }, + "expenseTrackerFilePath" : "data/fastTrack.json" +} diff --git a/src/main/java/seedu/address/AppParameters.java b/src/main/java/fasttrack/AppParameters.java similarity index 93% rename from src/main/java/seedu/address/AppParameters.java rename to src/main/java/fasttrack/AppParameters.java index ab552c398f3..6398e6451cc 100644 --- a/src/main/java/seedu/address/AppParameters.java +++ b/src/main/java/fasttrack/AppParameters.java @@ -1,4 +1,4 @@ -package seedu.address; +package fasttrack; import java.nio.file.Path; import java.nio.file.Paths; @@ -6,9 +6,9 @@ import java.util.Objects; import java.util.logging.Logger; +import fasttrack.commons.core.LogsCenter; +import fasttrack.commons.util.FileUtil; import javafx.application.Application; -import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.util.FileUtil; /** * Represents the parsed command-line parameters given to the application. diff --git a/src/main/java/seedu/address/Main.java b/src/main/java/fasttrack/Main.java similarity index 97% rename from src/main/java/seedu/address/Main.java rename to src/main/java/fasttrack/Main.java index 052a5068631..6fdb0bb06e5 100644 --- a/src/main/java/seedu/address/Main.java +++ b/src/main/java/fasttrack/Main.java @@ -1,4 +1,4 @@ -package seedu.address; +package fasttrack; import javafx.application.Application; diff --git a/src/main/java/seedu/address/MainApp.java b/src/main/java/fasttrack/MainApp.java similarity index 58% rename from src/main/java/seedu/address/MainApp.java rename to src/main/java/fasttrack/MainApp.java index 4133aaa0151..cbaf8d9d8eb 100644 --- a/src/main/java/seedu/address/MainApp.java +++ b/src/main/java/fasttrack/MainApp.java @@ -1,54 +1,54 @@ -package seedu.address; +package fasttrack; import java.io.IOException; import java.nio.file.Path; import java.util.Optional; import java.util.logging.Logger; +import fasttrack.commons.core.Config; +import fasttrack.commons.core.LogsCenter; +import fasttrack.commons.core.Version; +import fasttrack.commons.exceptions.DataConversionException; +import fasttrack.commons.util.ConfigUtil; +import fasttrack.commons.util.StringUtil; +import fasttrack.logic.Logic; +import fasttrack.logic.LogicManager; +import fasttrack.model.ExpenseTracker; +import fasttrack.model.Model; +import fasttrack.model.ModelManager; +import fasttrack.model.ReadOnlyExpenseTracker; +import fasttrack.model.ReadOnlyUserPrefs; +import fasttrack.model.UserPrefs; +import fasttrack.model.util.SampleExpenseTracker; +import fasttrack.storage.ExpenseTrackerStorage; +import fasttrack.storage.JsonExpenseTrackerStorage; +import fasttrack.storage.JsonUserPrefsStorage; +import fasttrack.storage.Storage; +import fasttrack.storage.StorageManager; +import fasttrack.storage.UserPrefsStorage; +import fasttrack.ui.Ui; +import fasttrack.ui.UiManager; import javafx.application.Application; import javafx.stage.Stage; -import seedu.address.commons.core.Config; -import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.core.Version; -import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.commons.util.ConfigUtil; -import seedu.address.commons.util.StringUtil; -import seedu.address.logic.Logic; -import seedu.address.logic.LogicManager; -import seedu.address.model.AddressBook; -import seedu.address.model.Model; -import seedu.address.model.ModelManager; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.ReadOnlyUserPrefs; -import seedu.address.model.UserPrefs; -import seedu.address.model.util.SampleDataUtil; -import seedu.address.storage.AddressBookStorage; -import seedu.address.storage.JsonAddressBookStorage; -import seedu.address.storage.JsonUserPrefsStorage; -import seedu.address.storage.Storage; -import seedu.address.storage.StorageManager; -import seedu.address.storage.UserPrefsStorage; -import seedu.address.ui.Ui; -import seedu.address.ui.UiManager; /** * Runs the application. */ public class MainApp extends Application { - public static final Version VERSION = new Version(0, 2, 0, true); + public static final Version VERSION = new Version(1, 4, 0, true); private static final Logger logger = LogsCenter.getLogger(MainApp.class); protected Ui ui; protected Logic logic; protected Storage storage; - protected Model model; + protected Model dataModel; protected Config config; @Override public void init() throws Exception { - logger.info("=============================[ Initializing AddressBook ]==========================="); + logger.info("=============================[ Initializing ExpenseTracker ]==========================="); super.init(); AppParameters appParameters = AppParameters.parse(getParameters()); @@ -56,39 +56,44 @@ public void init() throws Exception { UserPrefsStorage userPrefsStorage = new JsonUserPrefsStorage(config.getUserPrefsFilePath()); UserPrefs userPrefs = initPrefs(userPrefsStorage); - AddressBookStorage addressBookStorage = new JsonAddressBookStorage(userPrefs.getAddressBookFilePath()); - storage = new StorageManager(addressBookStorage, userPrefsStorage); + ExpenseTrackerStorage expenseTrackerStorage = new JsonExpenseTrackerStorage( + userPrefs.getExpenseTrackerFilePath()); + storage = new StorageManager(expenseTrackerStorage, userPrefsStorage); initLogging(config); - model = initModelManager(storage, userPrefs); + dataModel = initModelManager(storage, userPrefs); - logic = new LogicManager(model, storage); + logic = new LogicManager(dataModel, storage); ui = new UiManager(logic); } /** - * Returns a {@code ModelManager} with the data from {@code storage}'s address book and {@code userPrefs}.
- * The data from the sample address book will be used instead if {@code storage}'s address book is not found, - * or an empty address book will be used instead if errors occur when reading {@code storage}'s address book. + * Returns a {@code ModelManager} with the data from {@code storage}'s expense + * tracker and {@code userPrefs}.
+ * The data from the sample expense tracker will be used instead if + * {@code storage}'s expense tracker is not found, + * or an empty expense tracker will be used instead if errors occur when reading + * {@code storage}'s expense tracker. */ private Model initModelManager(Storage storage, ReadOnlyUserPrefs userPrefs) { - Optional addressBookOptional; - ReadOnlyAddressBook initialData; + Optional expenseTrackerOptional; + ReadOnlyExpenseTracker initialData; try { - addressBookOptional = storage.readAddressBook(); - if (!addressBookOptional.isPresent()) { - logger.info("Data file not found. Will be starting with a sample AddressBook"); + expenseTrackerOptional = storage.readExpenseTracker(); + if (!expenseTrackerOptional.isPresent()) { + logger.info("Data file not found. Will be starting with a sample ExpenseTracker"); } - initialData = addressBookOptional.orElseGet(SampleDataUtil::getSampleAddressBook); + initialData = expenseTrackerOptional.orElseGet(SampleExpenseTracker::getSampleExpenseTracker); } catch (DataConversionException e) { - logger.warning("Data file not in the correct format. Will be starting with an empty AddressBook"); - initialData = new AddressBook(); + logger.warning("Data file not in the correct format. Will be starting with an empty ExpenseTracker"); + initialData = new ExpenseTracker(); } catch (IOException e) { - logger.warning("Problem while reading from the file. Will be starting with an empty AddressBook"); - initialData = new AddressBook(); + logger.warning("Problem while reading from the file. Will be starting with an empty ExpenseTracker"); + initialData = new ExpenseTracker(); } + logger.info("fine"); return new ModelManager(initialData, userPrefs); } @@ -124,7 +129,8 @@ protected Config initConfig(Path configFilePath) { initializedConfig = new Config(); } - //Update config file in case it was missing to begin with or there are new/unused fields + // Update config file in case it was missing to begin with or there are + // new/unused fields try { ConfigUtil.saveConfig(initializedConfig, configFilePathUsed); } catch (IOException e) { @@ -134,7 +140,8 @@ protected Config initConfig(Path configFilePath) { } /** - * Returns a {@code UserPrefs} using the file at {@code storage}'s user prefs file path, + * Returns a {@code UserPrefs} using the file at {@code storage}'s user prefs + * file path, * or a new {@code UserPrefs} with default configuration if errors occur when * reading from the file. */ @@ -151,11 +158,12 @@ protected UserPrefs initPrefs(UserPrefsStorage storage) { + "Using default user prefs"); initializedPrefs = new UserPrefs(); } catch (IOException e) { - logger.warning("Problem while reading from the file. Will be starting with an empty AddressBook"); + logger.warning("Problem while reading from the file. Will be starting with an empty ExpenseTracker"); initializedPrefs = new UserPrefs(); } - //Update prefs file in case it was missing to begin with or there are new/unused fields + // Update prefs file in case it was missing to begin with or there are + // new/unused fields try { storage.saveUserPrefs(initializedPrefs); } catch (IOException e) { @@ -167,17 +175,18 @@ protected UserPrefs initPrefs(UserPrefsStorage storage) { @Override public void start(Stage primaryStage) { - logger.info("Starting AddressBook " + MainApp.VERSION); + logger.info("Starting ExpenseTracker " + MainApp.VERSION); ui.start(primaryStage); } @Override public void stop() { - logger.info("============================ [ Stopping Address Book ] ============================="); + logger.info("============================ [ Stopping Expense Tracker ] ============================="); try { - storage.saveUserPrefs(model.getUserPrefs()); + storage.saveUserPrefs(dataModel.getUserPrefs()); + storage.saveExpenseTracker(dataModel.getExpenseTracker()); } catch (IOException e) { - logger.severe("Failed to save preferences " + StringUtil.getDetails(e)); + logger.severe("Failed to save data " + StringUtil.getDetails(e)); } } } diff --git a/src/main/java/seedu/address/commons/core/Config.java b/src/main/java/fasttrack/commons/core/Config.java similarity index 97% rename from src/main/java/seedu/address/commons/core/Config.java rename to src/main/java/fasttrack/commons/core/Config.java index 91145745521..c61972610aa 100644 --- a/src/main/java/seedu/address/commons/core/Config.java +++ b/src/main/java/fasttrack/commons/core/Config.java @@ -1,4 +1,4 @@ -package seedu.address.commons.core; +package fasttrack.commons.core; import java.nio.file.Path; import java.nio.file.Paths; diff --git a/src/main/java/seedu/address/commons/core/GuiSettings.java b/src/main/java/fasttrack/commons/core/GuiSettings.java similarity index 98% rename from src/main/java/seedu/address/commons/core/GuiSettings.java rename to src/main/java/fasttrack/commons/core/GuiSettings.java index ba33653be67..48b5350237f 100644 --- a/src/main/java/seedu/address/commons/core/GuiSettings.java +++ b/src/main/java/fasttrack/commons/core/GuiSettings.java @@ -1,4 +1,4 @@ -package seedu.address.commons.core; +package fasttrack.commons.core; import java.awt.Point; import java.io.Serializable; diff --git a/src/main/java/seedu/address/commons/core/LogsCenter.java b/src/main/java/fasttrack/commons/core/LogsCenter.java similarity index 97% rename from src/main/java/seedu/address/commons/core/LogsCenter.java rename to src/main/java/fasttrack/commons/core/LogsCenter.java index 431e7185e76..5a68d747bc5 100644 --- a/src/main/java/seedu/address/commons/core/LogsCenter.java +++ b/src/main/java/fasttrack/commons/core/LogsCenter.java @@ -1,4 +1,4 @@ -package seedu.address.commons.core; +package fasttrack.commons.core; import java.io.IOException; import java.util.Arrays; @@ -18,7 +18,7 @@ public class LogsCenter { private static final int MAX_FILE_COUNT = 5; private static final int MAX_FILE_SIZE_IN_BYTES = (int) (Math.pow(2, 20) * 5); // 5MB - private static final String LOG_FILE = "addressbook.log"; + private static final String LOG_FILE = "fasttrack.log"; private static Level currentLogLevel = Level.INFO; private static final Logger logger = LogsCenter.getLogger(LogsCenter.class); private static FileHandler fileHandler; diff --git a/src/main/java/fasttrack/commons/core/Messages.java b/src/main/java/fasttrack/commons/core/Messages.java new file mode 100644 index 00000000000..5b4f7d90d66 --- /dev/null +++ b/src/main/java/fasttrack/commons/core/Messages.java @@ -0,0 +1,37 @@ +package fasttrack.commons.core; + +/** + * Container for user visible messages. + */ +public class Messages { + + public static final String MESSAGE_UNKNOWN_COMMAND = "Unknown command"; + public static final String MESSAGE_EXPENSES_LISTED_OVERVIEW = "%1$d expenses listed"; + + public static final String MESSAGE_INVALID_DATE_FORMAT = "Date should be of the form D/M/YY"; + + public static final String MESSAGE_INVALID_COMMAND_FORMAT = "Invalid command format! \n%1$s"; + public static final String MESSAGE_INVALID_INDEX = "The index provided is invalid."; + public static final String MESSAGE_INVALID_CATEGORY_DISPLAYED_INDEX = "The category index provided is invalid!"; + public static final String MESSAGE_INVALID_EXPENSE_DISPLAYED_INDEX = "The expense index provided is invalid!"; + public static final String MESSAGE_INVALID_RECURRING_EXPENSE_DISPLAYED_INDEX = "The recurring expense index " + + "provided is invalid!"; + public static final String MESSAGE_INVALID_EXPENSE_CATEGORY = "The provided category does not exist!"; + + public static final String MESSAGE_INVALID_EDIT_FOR_EXPENSE = "Please specify an edit to at least " + + "the date, name, price or category of the expense!"; + public static final String MESSAGE_INVALID_EDIT_FOR_CATEGORIES = "Please specify an edit to at least " + + "the category name or the category's summary."; + public static final String MESSAGE_SUCCESSFULLY_EDITED_CATEGORY = "Edited category: %1$s"; + public static final String MESSAGE_SUCCESSFULLY_EDITED_EXPENSE = "Edited expense: %1$s"; + public static final String MESSAGE_SUCCESSFULLY_EDITED_RECURRING = "Edited recurring expense generator: %1$s"; + public static final String MESSAGE_INVALID_ENUM_FOR_FREQUENCY = "The frequency provided is invalid!" + + "Please choose from the following: daily, weekly, monthly or yearly."; + public static final String MESSAGE_INVALID_CATEGORY_NAME = "Please provide a category name!"; + public static final String MESSAGE_INVALID_EXPENSE_NAME = "Please provide an expense name!"; + public static final String MESSAGE_ALREADY_EXISTING_CATEGORY = "This category name is already used!"; + public static final String MESSAGE_INVALID_BUDGET = "Please provide a valid budget!"; + + + +} diff --git a/src/main/java/seedu/address/commons/core/Version.java b/src/main/java/fasttrack/commons/core/Version.java similarity index 98% rename from src/main/java/seedu/address/commons/core/Version.java rename to src/main/java/fasttrack/commons/core/Version.java index 12142ec1e32..6e5717a6e29 100644 --- a/src/main/java/seedu/address/commons/core/Version.java +++ b/src/main/java/fasttrack/commons/core/Version.java @@ -1,4 +1,4 @@ -package seedu.address.commons.core; +package fasttrack.commons.core; import java.util.regex.Matcher; import java.util.regex.Pattern; diff --git a/src/main/java/seedu/address/commons/core/index/Index.java b/src/main/java/fasttrack/commons/core/index/Index.java similarity index 97% rename from src/main/java/seedu/address/commons/core/index/Index.java rename to src/main/java/fasttrack/commons/core/index/Index.java index 19536439c09..e8790792d74 100644 --- a/src/main/java/seedu/address/commons/core/index/Index.java +++ b/src/main/java/fasttrack/commons/core/index/Index.java @@ -1,4 +1,4 @@ -package seedu.address.commons.core.index; +package fasttrack.commons.core.index; /** * Represents a zero-based or one-based index. diff --git a/src/main/java/seedu/address/commons/exceptions/DataConversionException.java b/src/main/java/fasttrack/commons/exceptions/DataConversionException.java similarity index 84% rename from src/main/java/seedu/address/commons/exceptions/DataConversionException.java rename to src/main/java/fasttrack/commons/exceptions/DataConversionException.java index 1f689bd8e3f..9675779f614 100644 --- a/src/main/java/seedu/address/commons/exceptions/DataConversionException.java +++ b/src/main/java/fasttrack/commons/exceptions/DataConversionException.java @@ -1,4 +1,4 @@ -package seedu.address.commons.exceptions; +package fasttrack.commons.exceptions; /** * Represents an error during conversion of data from one format to another diff --git a/src/main/java/seedu/address/commons/exceptions/IllegalValueException.java b/src/main/java/fasttrack/commons/exceptions/IllegalValueException.java similarity index 93% rename from src/main/java/seedu/address/commons/exceptions/IllegalValueException.java rename to src/main/java/fasttrack/commons/exceptions/IllegalValueException.java index 19124db485c..b9d03c2d036 100644 --- a/src/main/java/seedu/address/commons/exceptions/IllegalValueException.java +++ b/src/main/java/fasttrack/commons/exceptions/IllegalValueException.java @@ -1,4 +1,4 @@ -package seedu.address.commons.exceptions; +package fasttrack.commons.exceptions; /** * Signals that some given data does not fulfill some constraints. diff --git a/src/main/java/seedu/address/commons/util/AppUtil.java b/src/main/java/fasttrack/commons/util/AppUtil.java similarity index 94% rename from src/main/java/seedu/address/commons/util/AppUtil.java rename to src/main/java/fasttrack/commons/util/AppUtil.java index 87aa89c0326..202944205c6 100644 --- a/src/main/java/seedu/address/commons/util/AppUtil.java +++ b/src/main/java/fasttrack/commons/util/AppUtil.java @@ -1,9 +1,9 @@ -package seedu.address.commons.util; +package fasttrack.commons.util; import static java.util.Objects.requireNonNull; +import fasttrack.MainApp; import javafx.scene.image.Image; -import seedu.address.MainApp; /** * A container for App specific utility functions diff --git a/src/main/java/seedu/address/commons/util/CollectionUtil.java b/src/main/java/fasttrack/commons/util/CollectionUtil.java similarity index 96% rename from src/main/java/seedu/address/commons/util/CollectionUtil.java rename to src/main/java/fasttrack/commons/util/CollectionUtil.java index eafe4dfd681..d0b57417e8a 100644 --- a/src/main/java/seedu/address/commons/util/CollectionUtil.java +++ b/src/main/java/fasttrack/commons/util/CollectionUtil.java @@ -1,4 +1,4 @@ -package seedu.address.commons.util; +package fasttrack.commons.util; import static java.util.Objects.requireNonNull; diff --git a/src/main/java/seedu/address/commons/util/ConfigUtil.java b/src/main/java/fasttrack/commons/util/ConfigUtil.java similarity index 77% rename from src/main/java/seedu/address/commons/util/ConfigUtil.java rename to src/main/java/fasttrack/commons/util/ConfigUtil.java index f7f8a2bd44c..b9a2c31b14b 100644 --- a/src/main/java/seedu/address/commons/util/ConfigUtil.java +++ b/src/main/java/fasttrack/commons/util/ConfigUtil.java @@ -1,11 +1,11 @@ -package seedu.address.commons.util; +package fasttrack.commons.util; import java.io.IOException; import java.nio.file.Path; import java.util.Optional; -import seedu.address.commons.core.Config; -import seedu.address.commons.exceptions.DataConversionException; +import fasttrack.commons.core.Config; +import fasttrack.commons.exceptions.DataConversionException; /** * A class for accessing the Config File. diff --git a/src/main/java/seedu/address/commons/util/FileUtil.java b/src/main/java/fasttrack/commons/util/FileUtil.java similarity index 98% rename from src/main/java/seedu/address/commons/util/FileUtil.java rename to src/main/java/fasttrack/commons/util/FileUtil.java index b1e2767cdd9..82d0415b43a 100644 --- a/src/main/java/seedu/address/commons/util/FileUtil.java +++ b/src/main/java/fasttrack/commons/util/FileUtil.java @@ -1,4 +1,4 @@ -package seedu.address.commons.util; +package fasttrack.commons.util; import java.io.IOException; import java.nio.file.Files; diff --git a/src/main/java/seedu/address/commons/util/JsonUtil.java b/src/main/java/fasttrack/commons/util/JsonUtil.java similarity index 97% rename from src/main/java/seedu/address/commons/util/JsonUtil.java rename to src/main/java/fasttrack/commons/util/JsonUtil.java index 8ef609f055d..4eb6c54ea2c 100644 --- a/src/main/java/seedu/address/commons/util/JsonUtil.java +++ b/src/main/java/fasttrack/commons/util/JsonUtil.java @@ -1,4 +1,4 @@ -package seedu.address.commons.util; +package fasttrack.commons.util; import static java.util.Objects.requireNonNull; @@ -20,8 +20,8 @@ import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; -import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.exceptions.DataConversionException; +import fasttrack.commons.core.LogsCenter; +import fasttrack.commons.exceptions.DataConversionException; /** * Converts a Java object instance to JSON and vice versa diff --git a/src/main/java/seedu/address/commons/util/StringUtil.java b/src/main/java/fasttrack/commons/util/StringUtil.java similarity index 84% rename from src/main/java/seedu/address/commons/util/StringUtil.java rename to src/main/java/fasttrack/commons/util/StringUtil.java index 61cc8c9a1cb..88298dfddc5 100644 --- a/src/main/java/seedu/address/commons/util/StringUtil.java +++ b/src/main/java/fasttrack/commons/util/StringUtil.java @@ -1,7 +1,7 @@ -package seedu.address.commons.util; +package fasttrack.commons.util; +import static fasttrack.commons.util.AppUtil.checkArgument; import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; import java.io.PrintWriter; import java.io.StringWriter; @@ -65,4 +65,16 @@ public static boolean isNonZeroUnsignedInteger(String s) { return false; } } + + /** + * Returns true if {@code s} represents a valid string for a field + */ + public static boolean isValidField(String s) { + if (s == null) { + return false; + } + // Regex to strip extra whitespace between words + s = s.replaceAll("\\s+", " "); + return s.matches("[\\p{Alnum}][\\p{Alnum} ]*"); + } } diff --git a/src/main/java/fasttrack/logic/Logic.java b/src/main/java/fasttrack/logic/Logic.java new file mode 100644 index 00000000000..79be168e08f --- /dev/null +++ b/src/main/java/fasttrack/logic/Logic.java @@ -0,0 +1,64 @@ +package fasttrack.logic; + +import java.nio.file.Path; + +import fasttrack.commons.core.GuiSettings; +import fasttrack.logic.commands.CommandResult; +import fasttrack.logic.commands.exceptions.CommandException; +import fasttrack.logic.parser.ParserUtil; +import fasttrack.logic.parser.exceptions.ParseException; +import fasttrack.model.Model; +import fasttrack.model.ReadOnlyExpenseTracker; +import fasttrack.model.category.Category; +import fasttrack.model.expense.Expense; +import fasttrack.model.expense.RecurringExpenseManager; +import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.ObservableList; + +/** + * API of the Logic component + */ +public interface Logic { + /** + * Executes the command and returns the result. + * @param commandText The command as entered by the user. + * @return the result of the command execution. + * @throws CommandException If an error occurs during command execution. + * @throws ParseException If an error occurs during parsing. + */ + CommandResult execute(String commandText) throws CommandException, ParseException; + + /** + * Returns the ExpenseTracker. + * @see Model#getExpenseTracker() + */ + ReadOnlyExpenseTracker getExpenseTracker(); + + /** Returns an unmodifiable view of the list of categories */ + ObservableList getFilteredCategoryList(); + + /** Returns an unmodifiable view of the filtered list of expenses */ + ObservableList getFilteredExpenseList(); + + /** Returns an unmodifiable view of the list of recurring expenses */ + ObservableList getRecurringExpenseManagerList(); + + SimpleObjectProperty getAppliedTimeSpanFilter(); + + SimpleObjectProperty getAppliedCategoryFilter(); + + /** + * Returns the user prefs' file path. + */ + Path getAddressBookFilePath(); + + /** + * Returns the user prefs' GUI settings. + */ + GuiSettings getGuiSettings(); + + /** + * Set the user prefs' GUI settings. + */ + void setGuiSettings(GuiSettings guiSettings); +} diff --git a/src/main/java/fasttrack/logic/LogicManager.java b/src/main/java/fasttrack/logic/LogicManager.java new file mode 100644 index 00000000000..d3e0290379b --- /dev/null +++ b/src/main/java/fasttrack/logic/LogicManager.java @@ -0,0 +1,106 @@ +package fasttrack.logic; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.logging.Logger; + +import fasttrack.commons.core.GuiSettings; +import fasttrack.commons.core.LogsCenter; +import fasttrack.logic.commands.Command; +import fasttrack.logic.commands.CommandResult; +import fasttrack.logic.commands.exceptions.CommandException; +import fasttrack.logic.parser.ExpenseTrackerParser; +import fasttrack.logic.parser.ParserUtil; +import fasttrack.logic.parser.exceptions.ParseException; +import fasttrack.model.Model; +import fasttrack.model.ReadOnlyExpenseTracker; +import fasttrack.model.category.Category; +import fasttrack.model.expense.Expense; +import fasttrack.model.expense.RecurringExpenseManager; +import fasttrack.storage.Storage; +import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.ObservableList; + +/** + * The main LogicManager of the app. + */ +public class LogicManager implements Logic { + public static final String FILE_OPS_ERROR_MESSAGE = "Could not save data to file: "; + private final Logger logger = LogsCenter.getLogger(LogicManager.class); + + private final Model dataModel; + private final Storage storage; + private final ExpenseTrackerParser expenseTrackerParser; + + /** + * Constructs a {@code LogicManager} with the given {@code Model} and + * {@code Storage}. + */ + public LogicManager(Model dataModel, Storage storage) { + this.dataModel = dataModel; + this.storage = storage; + expenseTrackerParser = new ExpenseTrackerParser(); + } + + @Override + public CommandResult execute(String commandText) throws CommandException, ParseException { + logger.info("----------------[USER COMMAND][" + commandText + "]"); + CommandResult commandResult; + Command command = expenseTrackerParser.parseCommand(commandText); + commandResult = command.execute(dataModel); + + try { + storage.saveExpenseTracker(dataModel.getExpenseTracker()); + } catch (IOException ioe) { + throw new CommandException(FILE_OPS_ERROR_MESSAGE + ioe, ioe); + } + + return commandResult; + } + + @Override + public ReadOnlyExpenseTracker getExpenseTracker() { + return dataModel.getExpenseTracker(); + } + + @Override + public ObservableList getFilteredCategoryList() { + return dataModel.getFilteredCategoryList(); + } + + @Override + public ObservableList getFilteredExpenseList() { + return dataModel.getFilteredExpenseList(); + } + + @Override + public ObservableList getRecurringExpenseManagerList() { + return dataModel.getRecurringExpenseGenerators(); + } + + @Override + public SimpleObjectProperty getAppliedTimeSpanFilter() { + return dataModel.getAppliedTimeSpanFilter(); + } + + @Override + public SimpleObjectProperty getAppliedCategoryFilter() { + return dataModel.getAppliedCategoryFilter(); + } + + @Override + public Path getAddressBookFilePath() { + return dataModel.getExpenseTrackerFilePath(); + } + + + @Override + public GuiSettings getGuiSettings() { + return dataModel.getGuiSettings(); + } + + @Override + public void setGuiSettings(GuiSettings guiSettings) { + dataModel.setGuiSettings(guiSettings); + } +} diff --git a/src/main/java/seedu/address/logic/commands/Command.java b/src/main/java/fasttrack/logic/commands/Command.java similarity index 50% rename from src/main/java/seedu/address/logic/commands/Command.java rename to src/main/java/fasttrack/logic/commands/Command.java index 64f18992160..8301310f285 100644 --- a/src/main/java/seedu/address/logic/commands/Command.java +++ b/src/main/java/fasttrack/logic/commands/Command.java @@ -1,20 +1,20 @@ -package seedu.address.logic.commands; +package fasttrack.logic.commands; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.model.Model; +import fasttrack.logic.commands.exceptions.CommandException; +import fasttrack.model.Model; /** * Represents a command with hidden internal logic and the ability to be executed. */ -public abstract class Command { +public interface Command { /** * Executes the command and returns the result message. * - * @param model {@code Model} which the command should operate on. + * @param dataModel {@code Model} which the command should operate on. * @return feedback message of the operation result for display * @throws CommandException If an error occurs during command execution. */ - public abstract CommandResult execute(Model model) throws CommandException; + public abstract CommandResult execute(Model dataModel) throws CommandException; } diff --git a/src/main/java/seedu/address/logic/commands/CommandResult.java b/src/main/java/fasttrack/logic/commands/CommandResult.java similarity index 52% rename from src/main/java/seedu/address/logic/commands/CommandResult.java rename to src/main/java/fasttrack/logic/commands/CommandResult.java index 92f900b7916..66005d0353d 100644 --- a/src/main/java/seedu/address/logic/commands/CommandResult.java +++ b/src/main/java/fasttrack/logic/commands/CommandResult.java @@ -1,9 +1,11 @@ -package seedu.address.logic.commands; +package fasttrack.logic.commands; import static java.util.Objects.requireNonNull; import java.util.Objects; +import fasttrack.ui.ScreenType; + /** * Represents the result of a command execution. */ @@ -17,21 +19,25 @@ public class CommandResult { /** The application should exit. */ private final boolean exit; + /** The screen to display upon execution of the command. */ + private final ScreenType screenType; + /** * Constructs a {@code CommandResult} with the specified fields. */ - public CommandResult(String feedbackToUser, boolean showHelp, boolean exit) { + public CommandResult(String feedbackToUser, boolean showHelp, boolean exit, ScreenType screenType) { this.feedbackToUser = requireNonNull(feedbackToUser); this.showHelp = showHelp; this.exit = exit; + this.screenType = screenType; } /** * Constructs a {@code CommandResult} with the specified {@code feedbackToUser}, * and other fields set to their default value. */ - public CommandResult(String feedbackToUser) { - this(feedbackToUser, false, false); + public CommandResult(String feedbackToUser, ScreenType screenType) { + this(feedbackToUser, false, false, screenType); } public String getFeedbackToUser() { @@ -46,21 +52,17 @@ public boolean isExit() { return exit; } + public ScreenType getScreenType() { + return screenType; + } + @Override public boolean equals(Object other) { - if (other == this) { - return true; - } - - // instanceof handles nulls - if (!(other instanceof CommandResult)) { - return false; - } - - CommandResult otherCommandResult = (CommandResult) other; - return feedbackToUser.equals(otherCommandResult.feedbackToUser) - && showHelp == otherCommandResult.showHelp - && exit == otherCommandResult.exit; + return other == this // short circuit if same object + || (other instanceof CommandResult // instanceof handles nulls + && feedbackToUser.equals(((CommandResult) other).feedbackToUser) + && showHelp == ((CommandResult) other).showHelp + && exit == ((CommandResult) other).exit); } @Override @@ -68,4 +70,14 @@ public int hashCode() { return Objects.hash(feedbackToUser, showHelp, exit); } + @Override + public String toString() { + return "CommandResult{" + + "feedbackToUser='" + feedbackToUser + '\'' + + ", showHelp=" + showHelp + + ", exit=" + exit + + ", screenType=" + screenType + + '}'; + } + } diff --git a/src/main/java/fasttrack/logic/commands/SetBudgetCommand.java b/src/main/java/fasttrack/logic/commands/SetBudgetCommand.java new file mode 100644 index 00000000000..18b06c4b65e --- /dev/null +++ b/src/main/java/fasttrack/logic/commands/SetBudgetCommand.java @@ -0,0 +1,51 @@ +package fasttrack.logic.commands; + +import static fasttrack.logic.parser.CliSyntax.PREFIX_PRICE; + +import fasttrack.model.Budget; +import fasttrack.model.Model; +import fasttrack.ui.ScreenType; + +/** + * Sets the monthly budget for FastTrack. + */ +public class SetBudgetCommand implements Command { + + + public static final String COMMAND_WORD = "set"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Sets monthly budget\n" + + "Parameters: " + + PREFIX_PRICE + "BUDGET_AMOUNT\n" + + "Example: " + COMMAND_WORD + " " + PREFIX_PRICE + "1000"; + + public static final String MESSAGE_SUCCESS = "Monthly budget successfully set to "; + private final Budget budget; + + public SetBudgetCommand(Budget budget) { + this.budget = budget; + } + + @Override + public CommandResult execute(Model dataModel) { + dataModel.setBudget(budget); + return new CommandResult(MESSAGE_SUCCESS + this.budget, ScreenType.EXPENSE_SCREEN); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof SetBudgetCommand // instanceof handles nulls + && budget.equals(((SetBudgetCommand) other).budget)); + } + + @Override + public String toString() { + return "SetBudgetCommand{budget=" + budget + '}'; + } + + @Override + public int hashCode() { + return budget.hashCode(); + } +} diff --git a/src/main/java/fasttrack/logic/commands/add/AddCategoryCommand.java b/src/main/java/fasttrack/logic/commands/add/AddCategoryCommand.java new file mode 100644 index 00000000000..b5fd20a346e --- /dev/null +++ b/src/main/java/fasttrack/logic/commands/add/AddCategoryCommand.java @@ -0,0 +1,55 @@ +package fasttrack.logic.commands.add; + +import static fasttrack.logic.parser.CliSyntax.PREFIX_CATEGORY; +import static fasttrack.logic.parser.CliSyntax.PREFIX_SUMMARY; +import static java.util.Objects.requireNonNull; + +import fasttrack.logic.commands.CommandResult; +import fasttrack.logic.commands.exceptions.CommandException; +import fasttrack.model.Model; +import fasttrack.model.category.Category; +import fasttrack.ui.ScreenType; + +/** + * Adds a category to the Expense Tracker. + */ +public class AddCategoryCommand implements AddCommand { + + public static final String COMMAND_WORD = "addcat"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a category to FastTrack. " + + "Parameters: " + + PREFIX_CATEGORY + "CATEGORY_NAME " + + "[" + PREFIX_SUMMARY + "CATEGORY_SUMMARY]\n" + + "Example: " + COMMAND_WORD + " " + + PREFIX_CATEGORY + "groceries " + + PREFIX_SUMMARY + "all expenses related to groceries\n"; + public static final String MESSAGE_SUCCESS = "New category added: %1$s"; + public static final String MESSAGE_DUPLICATE_CATEGORY = "This category already exists in FastTrack"; + + private final Category toAdd; + + /** + * Creates an AddCategoryCommand to add the specified {@code Category} + */ + public AddCategoryCommand(Category category) { + requireNonNull(category); + toAdd = category; + } + + @Override + public CommandResult execute(Model dataModel) throws CommandException { + requireNonNull(dataModel); + if (dataModel.hasCategory(toAdd)) { + throw new CommandException(MESSAGE_DUPLICATE_CATEGORY); + } + dataModel.addCategory(toAdd); + return new CommandResult(String.format(MESSAGE_SUCCESS, toAdd), ScreenType.CATEGORY_SCREEN); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof AddCategoryCommand // instanceof handles nulls + && toAdd.equals(((AddCategoryCommand) other).toAdd)); + } +} diff --git a/src/main/java/fasttrack/logic/commands/add/AddCommand.java b/src/main/java/fasttrack/logic/commands/add/AddCommand.java new file mode 100644 index 00000000000..7430248e9f7 --- /dev/null +++ b/src/main/java/fasttrack/logic/commands/add/AddCommand.java @@ -0,0 +1,9 @@ +package fasttrack.logic.commands.add; + +import fasttrack.logic.commands.Command; + +/** + * Represents an add command with hidden internal logic and the ability to be + * executed. + */ +public interface AddCommand extends Command {} diff --git a/src/main/java/fasttrack/logic/commands/add/AddExpenseCommand.java b/src/main/java/fasttrack/logic/commands/add/AddExpenseCommand.java new file mode 100644 index 00000000000..200fe0e1300 --- /dev/null +++ b/src/main/java/fasttrack/logic/commands/add/AddExpenseCommand.java @@ -0,0 +1,67 @@ +package fasttrack.logic.commands.add; + +import static fasttrack.logic.parser.CliSyntax.PREFIX_CATEGORY; +import static fasttrack.logic.parser.CliSyntax.PREFIX_DATE; +import static fasttrack.logic.parser.CliSyntax.PREFIX_NAME; +import static fasttrack.logic.parser.CliSyntax.PREFIX_PRICE; +import static java.util.Objects.requireNonNull; + +import fasttrack.logic.commands.CommandResult; +import fasttrack.logic.commands.exceptions.CommandException; +import fasttrack.model.Model; +import fasttrack.model.category.Category; +import fasttrack.model.expense.Expense; +import fasttrack.ui.ScreenType; + + +/** + * Adds an expense to the Expense Tracker. + */ +public class AddExpenseCommand implements AddCommand { + + public static final String COMMAND_WORD = "add"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds an expense to FastTrack " + + "Parameters: " + + PREFIX_NAME + "EXPENSE NAME " + + PREFIX_CATEGORY + "CATEGORY " + + PREFIX_PRICE + "AMOUNT " + + "[" + PREFIX_DATE + "DATE]\n" + + "Example: " + COMMAND_WORD + " " + + PREFIX_NAME + "Milk " + + PREFIX_CATEGORY + "groceries " + + PREFIX_PRICE + "4.50 " + + PREFIX_DATE + "2/10/23"; + + public static final String MESSAGE_SUCCESS = "New expense added: %1$s"; + + private final Expense newExpense; + + /** + * Creates an AddExpenseCommand to add the specified {@code Expense} + */ + public AddExpenseCommand(Expense expense) { + requireNonNull(expense); + newExpense = expense; + } + + @Override + public CommandResult execute(Model dataModel) throws CommandException { + requireNonNull(dataModel); + Category newCategory = newExpense.getCategory(); + Category existingCategory = dataModel.getCategoryInstance(newCategory); + if (existingCategory != null) { + newExpense.setCategory(existingCategory); + } else { + dataModel.addCategory(newCategory); + } + dataModel.addExpense(newExpense); + return new CommandResult(String.format(MESSAGE_SUCCESS, newExpense), ScreenType.EXPENSE_SCREEN); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof AddExpenseCommand // instanceof handles nulls + && newExpense.equals(((AddExpenseCommand) other).newExpense)); + } +} diff --git a/src/main/java/fasttrack/logic/commands/add/AddRecurringExpenseCommand.java b/src/main/java/fasttrack/logic/commands/add/AddRecurringExpenseCommand.java new file mode 100644 index 00000000000..a94bbf4613a --- /dev/null +++ b/src/main/java/fasttrack/logic/commands/add/AddRecurringExpenseCommand.java @@ -0,0 +1,79 @@ +package fasttrack.logic.commands.add; + +import static fasttrack.logic.parser.CliSyntax.PREFIX_CATEGORY; +import static fasttrack.logic.parser.CliSyntax.PREFIX_END_DATE; +import static fasttrack.logic.parser.CliSyntax.PREFIX_NAME; +import static fasttrack.logic.parser.CliSyntax.PREFIX_PRICE; +import static fasttrack.logic.parser.CliSyntax.PREFIX_START_DATE; +import static fasttrack.logic.parser.CliSyntax.PREFIX_TIMESPAN; +import static java.util.Objects.requireNonNull; + +import fasttrack.logic.commands.CommandResult; +import fasttrack.logic.commands.exceptions.CommandException; +import fasttrack.model.Model; +import fasttrack.model.category.Category; +import fasttrack.model.expense.RecurringExpenseManager; +import fasttrack.ui.ScreenType; + +/** + * Adds a category to the Expense Tracker. + */ +public class AddRecurringExpenseCommand implements AddCommand { + + public static final String COMMAND_WORD = "addrec"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a recurring expense to FastTrack. " + + "Parameters: " + + PREFIX_NAME + "RECURRING_EXPENSE_NAME" + + PREFIX_CATEGORY + "CATEGORY_NAME " + + PREFIX_PRICE + "AMOUNT " + + PREFIX_TIMESPAN + "TIMESPAN " + + PREFIX_START_DATE + "START_DATE " + + "[" + PREFIX_END_DATE + "END_DATE]\n" + + "Example: " + COMMAND_WORD + " " + + PREFIX_NAME + "Netflix " + + PREFIX_CATEGORY + "Subscription " + + PREFIX_PRICE + "10 " + + PREFIX_TIMESPAN + "week " + + PREFIX_START_DATE + "01/03/23 " + + PREFIX_END_DATE + "01/03/24\n"; + public static final String MESSAGE_SUCCESS = "New recurring expense added: %1$s"; + public static final String MESSAGE_DUPLICATE_RECURRING_EXPENSE = + "This recurring expense already exists in FastTrack"; + + private final RecurringExpenseManager toAdd; + + /** + * Creates an AddCategoryCommand to add the specified {@code Category} + */ + public AddRecurringExpenseCommand(RecurringExpenseManager toAdd) { + requireNonNull(toAdd); + this.toAdd = toAdd; + } + + @Override + public CommandResult execute(Model dataModel) throws CommandException { + requireNonNull(dataModel); + if (dataModel.hasRecurringExpense(toAdd)) { + throw new CommandException(MESSAGE_DUPLICATE_RECURRING_EXPENSE); + } + + Category newCategory = toAdd.getExpenseCategory(); + Category existingCategory = dataModel.getCategoryInstance(newCategory); + if (existingCategory != null) { + toAdd.setExpenseCategory(existingCategory); + } else { + dataModel.addCategory(newCategory); + } + + dataModel.addRecurringGenerator(toAdd); + dataModel.addRetroactiveExpenses(); + return new CommandResult(String.format(MESSAGE_SUCCESS, toAdd), ScreenType.RECURRING_EXPENSE_SCREEN); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof AddRecurringExpenseCommand // instanceof handles nulls + && toAdd.equals(((AddRecurringExpenseCommand) other).toAdd)); + } +} diff --git a/src/main/java/fasttrack/logic/commands/delete/DeleteCategoryCommand.java b/src/main/java/fasttrack/logic/commands/delete/DeleteCategoryCommand.java new file mode 100644 index 00000000000..5eaea084a86 --- /dev/null +++ b/src/main/java/fasttrack/logic/commands/delete/DeleteCategoryCommand.java @@ -0,0 +1,61 @@ +package fasttrack.logic.commands.delete; + +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import fasttrack.commons.core.Messages; +import fasttrack.commons.core.index.Index; +import fasttrack.logic.commands.CommandResult; +import fasttrack.logic.commands.exceptions.CommandException; +import fasttrack.model.Model; +import fasttrack.model.category.Category; +import fasttrack.ui.ScreenType; + +/** + * Deletes a category identified using it's displayed index from the expense tracker. + */ +public class DeleteCategoryCommand implements DeleteCommand { + + public static final String COMMAND_WORD = "delcat"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Deletes the category identified by the index number used in the displayed category list.\n" + + "Parameters: INDEX (must be a positive integer)\n" + + "Example: " + COMMAND_WORD + " 1"; + + public static final String MESSAGE_DELETE_CATEGORY_SUCCESS = "Deleted category: %1$s"; + + private final Index targetIndex; + + /** + * Creates an DeleteCategory to delete the specified {@code Category} + * @param targetIndex index of the category in the filtered category list to delete + */ + public DeleteCategoryCommand(Index targetIndex) { + this.targetIndex = targetIndex; + } + + @Override + public CommandResult execute(Model dataModel) throws CommandException { + requireNonNull(dataModel); + List lastShownList = dataModel.getFilteredCategoryList(); + + if (targetIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_CATEGORY_DISPLAYED_INDEX); + } + + Category categoryToDelete = lastShownList.get(targetIndex.getZeroBased()); + dataModel.deleteCategory(categoryToDelete); + return new CommandResult( + String.format(MESSAGE_DELETE_CATEGORY_SUCCESS, categoryToDelete), + ScreenType.CATEGORY_SCREEN); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof DeleteCategoryCommand // instanceof handles nulls + && targetIndex.equals(((DeleteCategoryCommand) other).targetIndex)); // state check + } +} diff --git a/src/main/java/fasttrack/logic/commands/delete/DeleteCommand.java b/src/main/java/fasttrack/logic/commands/delete/DeleteCommand.java new file mode 100644 index 00000000000..0d8fbc2fd42 --- /dev/null +++ b/src/main/java/fasttrack/logic/commands/delete/DeleteCommand.java @@ -0,0 +1,9 @@ +package fasttrack.logic.commands.delete; + +import fasttrack.logic.commands.Command; + +/** + * Represents a delete command with hidden internal logic and the ability to be executed. + */ +public interface DeleteCommand extends Command { +} diff --git a/src/main/java/fasttrack/logic/commands/delete/DeleteExpenseCommand.java b/src/main/java/fasttrack/logic/commands/delete/DeleteExpenseCommand.java new file mode 100644 index 00000000000..7a8b7fddf68 --- /dev/null +++ b/src/main/java/fasttrack/logic/commands/delete/DeleteExpenseCommand.java @@ -0,0 +1,57 @@ +package fasttrack.logic.commands.delete; + +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import fasttrack.commons.core.Messages; +import fasttrack.commons.core.index.Index; +import fasttrack.logic.commands.CommandResult; +import fasttrack.logic.commands.exceptions.CommandException; +import fasttrack.model.Model; +import fasttrack.model.expense.Expense; +import fasttrack.ui.ScreenType; + +/** + * Deletes an expense from the expense tracker. + */ +public class DeleteExpenseCommand implements DeleteCommand { + + public static final String COMMAND_WORD = "delete"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Deletes the expense identified by the index number used in the displayed expenses list.\n" + + "Parameters: INDEX (must be a positive integer)\n" + + "Example: " + COMMAND_WORD + " 1"; + + public static final String MESSAGE_DELETE_EXPENSE_SUCCESS = "Deleted expense: %1$s"; + private final Index targetIndex; + /** + * Creates an DeleteExpenseCommand to delete the specified {@code Expense} + * @param targetIndex index of the expense in the filtered expense list to delete + */ + public DeleteExpenseCommand(Index targetIndex) { + this.targetIndex = targetIndex; + } + + @Override + public CommandResult execute(Model dataModel) throws CommandException { + requireNonNull(dataModel); + List lastShownList = dataModel.getFilteredExpenseList(); + + if (targetIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_INDEX); + } + + Expense expense = lastShownList.get(targetIndex.getZeroBased()); + dataModel.deleteExpense(expense); + return new CommandResult(String.format(MESSAGE_DELETE_EXPENSE_SUCCESS, expense), ScreenType.EXPENSE_SCREEN); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof DeleteExpenseCommand // instanceof handles nulls + && targetIndex.equals(((DeleteExpenseCommand) other).targetIndex)); // state check + } +} diff --git a/src/main/java/fasttrack/logic/commands/delete/DeleteRecurringExpenseCommand.java b/src/main/java/fasttrack/logic/commands/delete/DeleteRecurringExpenseCommand.java new file mode 100644 index 00000000000..a4b8db7c72e --- /dev/null +++ b/src/main/java/fasttrack/logic/commands/delete/DeleteRecurringExpenseCommand.java @@ -0,0 +1,59 @@ +package fasttrack.logic.commands.delete; + +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import fasttrack.commons.core.Messages; +import fasttrack.commons.core.index.Index; +import fasttrack.logic.commands.CommandResult; +import fasttrack.logic.commands.exceptions.CommandException; +import fasttrack.model.Model; +import fasttrack.model.expense.RecurringExpenseManager; +import fasttrack.ui.ScreenType; + +/** + * Deletes an expense from the expense tracker. + */ +public class DeleteRecurringExpenseCommand implements DeleteCommand { + + public static final String COMMAND_WORD = "delrec"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Deletes the recurring expense identified by the index number used in the displayed " + + "recurring expenses list.\n" + + "Parameters: INDEX (must be a positive integer)\n" + + "Example: " + COMMAND_WORD + " 1"; + + public static final String MESSAGE_DELETE_RECURRING_EXPENSE_SUCCESS = "Deleted recurring expense: %1$s"; + private final Index targetIndex; + /** + * Creates an DeleteExpenseCommand to delete the specified {@code Expense} + * @param targetIndex index of the expense in the filtered expense list to delete + */ + public DeleteRecurringExpenseCommand(Index targetIndex) { + this.targetIndex = targetIndex; + } + + @Override + public CommandResult execute(Model dataModel) throws CommandException { + requireNonNull(dataModel); + List lastShownList = dataModel.getRecurringExpenseGenerators(); + + if (targetIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_RECURRING_EXPENSE_DISPLAYED_INDEX); + } + + RecurringExpenseManager recurringExpenseManager = lastShownList.get(targetIndex.getZeroBased()); + dataModel.deleteRecurringExpense(recurringExpenseManager); + return new CommandResult(String.format(MESSAGE_DELETE_RECURRING_EXPENSE_SUCCESS, recurringExpenseManager), + ScreenType.RECURRING_EXPENSE_SCREEN); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof DeleteRecurringExpenseCommand // instanceof handles nulls + && targetIndex.equals(((DeleteRecurringExpenseCommand) other).targetIndex)); // state check + } +} diff --git a/src/main/java/fasttrack/logic/commands/edit/EditCategoryCommand.java b/src/main/java/fasttrack/logic/commands/edit/EditCategoryCommand.java new file mode 100644 index 00000000000..346071db623 --- /dev/null +++ b/src/main/java/fasttrack/logic/commands/edit/EditCategoryCommand.java @@ -0,0 +1,96 @@ +package fasttrack.logic.commands.edit; + +import static fasttrack.logic.commands.add.AddCategoryCommand.MESSAGE_DUPLICATE_CATEGORY; +import static fasttrack.logic.parser.CliSyntax.PREFIX_CATEGORY; +import static fasttrack.logic.parser.CliSyntax.PREFIX_SUMMARY; +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import fasttrack.commons.core.Messages; +import fasttrack.commons.core.index.Index; +import fasttrack.logic.commands.CommandResult; +import fasttrack.logic.commands.exceptions.CommandException; +import fasttrack.model.Model; +import fasttrack.model.category.Category; +import fasttrack.model.category.UserDefinedCategory; +import fasttrack.ui.ScreenType; + + +/** + * Edits a category in the ExpenseTracker + */ +public class EditCategoryCommand implements EditCommand { + public static final String COMMAND_WORD = "edcat"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Edits the category identified by the index number used in the displayed category list.\n" + + "Parameters: INDEX (must be a positive integer)\n" + + "[" + PREFIX_CATEGORY + "CATEGORY] " + + "[" + PREFIX_SUMMARY + "SUMMARY] " + + "Example: " + COMMAND_WORD + " edcat 1 c/food s/for meals"; + + private final Index targetIndex; + + private final String newCategoryName; + + private final String newSummary; + + /** + * Creates an EditCategory to edit the specified {@code Category} + * @param targetIndex index of the expense in the filtered category list to edit. + * @param newCategoryName String representation of the new category name to be edited to, if applicable. + * @param newSummary String representation of the new summary to be edited to, if applicable. + */ + public EditCategoryCommand(Index targetIndex, String newCategoryName, String newSummary) { + requireNonNull(targetIndex); + this.targetIndex = targetIndex; + this.newCategoryName = newCategoryName; + this.newSummary = newSummary; + } + + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredCategoryList(); + + //Check whether targetIndex is a valid index. + if (targetIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_CATEGORY_DISPLAYED_INDEX); + } + UserDefinedCategory categoryToEdit = (UserDefinedCategory) lastShownList.get(targetIndex.getZeroBased()); + + if (newCategoryName != null) { + + if (newCategoryName.isBlank()) { + throw new CommandException(Messages.MESSAGE_INVALID_CATEGORY_NAME); + } + for (Category category : lastShownList) { + if (category.getCategoryName().equalsIgnoreCase(newCategoryName)) { + throw new CommandException(MESSAGE_DUPLICATE_CATEGORY); + } + } + categoryToEdit.setCategoryName(newCategoryName.replaceAll("\\s+", " ")); + } + if (newSummary != null) { + categoryToEdit.setDescription(newSummary.replaceAll("\\s+", " ")); + } + + if (newCategoryName == null && newSummary == null) { + throw new CommandException(Messages.MESSAGE_INVALID_EDIT_FOR_CATEGORIES); + } + + return new CommandResult( + String.format(Messages.MESSAGE_SUCCESSFULLY_EDITED_CATEGORY, + categoryToEdit), ScreenType.CATEGORY_SCREEN); + + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof EditCategoryCommand // instanceof handles nulls + && targetIndex.equals(((EditCategoryCommand) other).targetIndex)); // state check + } +} diff --git a/src/main/java/fasttrack/logic/commands/edit/EditCommand.java b/src/main/java/fasttrack/logic/commands/edit/EditCommand.java new file mode 100644 index 00000000000..cf25e8cb821 --- /dev/null +++ b/src/main/java/fasttrack/logic/commands/edit/EditCommand.java @@ -0,0 +1,11 @@ +package fasttrack.logic.commands.edit; + +import fasttrack.logic.commands.Command; + +/** + * Represents a edit command with hidden internal logic and the ability to be + * executed. + */ +public interface EditCommand extends Command { + +} diff --git a/src/main/java/fasttrack/logic/commands/edit/EditExpenseCommand.java b/src/main/java/fasttrack/logic/commands/edit/EditExpenseCommand.java new file mode 100644 index 00000000000..881a8ad6261 --- /dev/null +++ b/src/main/java/fasttrack/logic/commands/edit/EditExpenseCommand.java @@ -0,0 +1,138 @@ +package fasttrack.logic.commands.edit; + +import static fasttrack.logic.parser.CliSyntax.PREFIX_CATEGORY; +import static fasttrack.logic.parser.CliSyntax.PREFIX_DATE; +import static fasttrack.logic.parser.CliSyntax.PREFIX_NAME; +import static fasttrack.logic.parser.CliSyntax.PREFIX_PRICE; +import static java.util.Objects.requireNonNull; + +import java.time.LocalDate; +import java.util.List; +import java.util.Objects; + +import fasttrack.commons.core.Messages; +import fasttrack.commons.core.index.Index; +import fasttrack.logic.commands.CommandResult; +import fasttrack.logic.commands.exceptions.CommandException; +import fasttrack.model.Model; +import fasttrack.model.category.Category; +import fasttrack.model.expense.Expense; +import fasttrack.model.expense.Price; +import fasttrack.ui.ScreenType; + +/** + * Edits an Expense in the expense tracker. + */ +public class EditExpenseCommand implements EditCommand { + public static final String COMMAND_WORD = "edexp"; + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Edits the expense identified by the index number used in the displayed expenses list.\n" + + "Parameters: INDEX (must be a positive integer)\n" + + "[" + PREFIX_NAME + "EXPENSE NAME] " + + "[" + PREFIX_CATEGORY + "CATEGORY] " + + "[" + PREFIX_PRICE + "AMOUNT] " + + "[" + PREFIX_DATE + "DATE] \n" + + "Example: " + COMMAND_WORD + " 1 n/KFC c/food p/10 d/20/03/23 "; + + private final Index targetIndex; + private final String newExpenseName; + private final Double newExpenseAmount; + private final LocalDate newExpenseDate; + private final String newExpenseCategoryInString; + + + /** + * JavaDoc + * @param targetIndex xx + * @param newExpenseName xx + * @param newExpenseAmount xx + * @param newExpenseDate xx + * @param newExpenseCategory xx + */ + public EditExpenseCommand(Index targetIndex, String newExpenseName, Double newExpenseAmount, + LocalDate newExpenseDate, String newExpenseCategory) { + requireNonNull(targetIndex); + this.targetIndex = targetIndex; + this.newExpenseName = newExpenseName; + this.newExpenseAmount = newExpenseAmount; + this.newExpenseDate = newExpenseDate; + this.newExpenseCategoryInString = newExpenseCategory; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownListOfExpenses = model.getFilteredExpenseList(); + List lastShownListOfCategories = model.getFilteredCategoryList(); + Category toBeAllocated = null; + + for (Category category : lastShownListOfCategories) { + if (category.getCategoryName().equalsIgnoreCase(this.newExpenseCategoryInString)) { + toBeAllocated = category; + break; + } + } + + if (newExpenseName == null && newExpenseAmount == null + && newExpenseDate == null && newExpenseCategoryInString == null) { + throw new CommandException(Messages.MESSAGE_INVALID_EDIT_FOR_EXPENSE); + } + + if (targetIndex.getZeroBased() >= lastShownListOfExpenses.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_EXPENSE_DISPLAYED_INDEX); + } + + Expense expenseToEdit = lastShownListOfExpenses.get(targetIndex.getZeroBased()); + String name = expenseToEdit.getName(); + double amount = expenseToEdit.getAmount(); + + Category category = expenseToEdit.getCategory(); + LocalDate date = expenseToEdit.getDate(); + + if (toBeAllocated != null) { + category = toBeAllocated; + } else if (this.newExpenseCategoryInString != null) { + throw new CommandException(Messages.MESSAGE_INVALID_EXPENSE_CATEGORY); + } + + if (newExpenseName != null) { + if (newExpenseName.stripTrailing().isEmpty()) { + throw new CommandException(Messages.MESSAGE_INVALID_EXPENSE_NAME); + } + name = newExpenseName; + } + + if (newExpenseAmount != null) { + if (!Price.isValidPrice(String.valueOf(newExpenseAmount))) { + throw new CommandException(Price.MESSAGE_CONSTRAINTS); + } + amount = newExpenseAmount; + } + + if (newExpenseDate != null) { + date = newExpenseDate; + } + + Expense newExpense = new Expense(name, amount, date, category); + model.setExpense(expenseToEdit, newExpense); + return new CommandResult( + String.format(Messages.MESSAGE_SUCCESSFULLY_EDITED_EXPENSE, newExpense), + ScreenType.EXPENSE_SCREEN); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (!(other instanceof EditExpenseCommand)) { + return false; + } + EditExpenseCommand otherTypeCasted = (EditExpenseCommand) other; + return targetIndex.equals(otherTypeCasted.targetIndex) + && (Objects.equals(newExpenseName, otherTypeCasted.newExpenseName)) + && (Objects.equals(newExpenseAmount, otherTypeCasted.newExpenseAmount)) + && (Objects.equals(newExpenseDate, otherTypeCasted.newExpenseDate)) + && (Objects.equals(newExpenseCategoryInString, otherTypeCasted.newExpenseCategoryInString)); + } +} diff --git a/src/main/java/fasttrack/logic/commands/edit/EditRecurringExpenseManagerCommand.java b/src/main/java/fasttrack/logic/commands/edit/EditRecurringExpenseManagerCommand.java new file mode 100644 index 00000000000..f3d9231e641 --- /dev/null +++ b/src/main/java/fasttrack/logic/commands/edit/EditRecurringExpenseManagerCommand.java @@ -0,0 +1,154 @@ +package fasttrack.logic.commands.edit; + +import static fasttrack.logic.parser.CliSyntax.PREFIX_CATEGORY; +import static fasttrack.logic.parser.CliSyntax.PREFIX_END_DATE; +import static fasttrack.logic.parser.CliSyntax.PREFIX_NAME; +import static fasttrack.logic.parser.CliSyntax.PREFIX_PRICE; +import static fasttrack.logic.parser.CliSyntax.PREFIX_TIMESPAN; +import static java.util.Objects.requireNonNull; + +import java.time.LocalDate; +import java.util.List; +import java.util.Objects; + +import fasttrack.commons.core.Messages; +import fasttrack.commons.core.index.Index; +import fasttrack.logic.commands.CommandResult; +import fasttrack.logic.commands.exceptions.CommandException; +import fasttrack.model.Model; +import fasttrack.model.category.Category; +import fasttrack.model.expense.Price; +import fasttrack.model.expense.RecurringExpenseManager; +import fasttrack.model.expense.RecurringExpenseType; +import fasttrack.ui.ScreenType; + +/** + * Edits a RecurringExpenseManager in the ExpenseTracker + */ +public class EditRecurringExpenseManagerCommand implements EditCommand { + public static final String COMMAND_WORD = "edrec"; + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Edits the recurring expense identified by the index number used in the displayed recurring" + + " expenses list.\n" + + "Parameters: INDEX (must be a positive integer)\n" + + "[" + PREFIX_NAME + "EXPENSE NAME] " + + "[" + PREFIX_CATEGORY + "CATEGORY] " + + "[" + PREFIX_PRICE + "AMOUNT] " + + "[" + PREFIX_TIMESPAN + "FREQUENCY] \n" + + "[" + PREFIX_END_DATE + "END-DATE] \n" + + "Example: " + COMMAND_WORD + " 1 n/KFC c/food p/10 t/weekly ed/20/03/23 "; + + private final Index targetIndex; + private final String newExpenseName; + private final String newExpenseCategoryInString; + private final Double newExpenseAmount; + private final String newFrequencyInString; + private final LocalDate newExpenseEndDate; + + /** + * Creates an EditRecurringExpenseManagerCommand to edit the specified + * {@code RecurringExpenseManager} + * @param targetIndex index of the recurringexpensemanager in the + * filtered recurringexpensemanager list to + * edit. + * @param newExpenseName String representation of the new category + * name to be edited to, if applicable. + * @param newExpenseAmount New expense price to be edited to, if + * applicable. + * @param newExpenseCategoryInString String representation of the new category's + * name to be edited to, + * if applicable. + * @param newFrequencyInString String representation of the frequency to + * be edited to, if applicable. + * @param newExpenseEndDate New recurring expense end date to be edited + * to, if applicable. + */ + public EditRecurringExpenseManagerCommand(Index targetIndex, String newExpenseName, Double newExpenseAmount, + String newExpenseCategoryInString, String newFrequencyInString, + LocalDate newExpenseEndDate) { + requireNonNull(targetIndex); + this.targetIndex = targetIndex; + this.newExpenseName = newExpenseName; + this.newExpenseAmount = newExpenseAmount; + this.newExpenseCategoryInString = newExpenseCategoryInString; + this.newFrequencyInString = newFrequencyInString; + this.newExpenseEndDate = newExpenseEndDate; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownListOfCategories = model.getFilteredCategoryList(); + List lastShownListOfRecurringExpenseManagers = model.getRecurringExpenseGenerators(); + Category toBeAllocated = null; + + for (Category category : lastShownListOfCategories) { + if (category.getCategoryName().equalsIgnoreCase(this.newExpenseCategoryInString)) { + toBeAllocated = category; + break; + } + } + + if (newExpenseName == null && newExpenseAmount == null + && newFrequencyInString == null && newExpenseCategoryInString == null + && this.newExpenseEndDate == null) { + throw new CommandException(Messages.MESSAGE_INVALID_EDIT_FOR_EXPENSE); + } + + // Check if index is valid + if (targetIndex == null || targetIndex.getZeroBased() >= lastShownListOfRecurringExpenseManagers.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_EXPENSE_DISPLAYED_INDEX); + } + + RecurringExpenseManager generatorToEdit = lastShownListOfRecurringExpenseManagers + .get(targetIndex.getZeroBased()); + + if (toBeAllocated != null) { + generatorToEdit.setExpenseCategory(toBeAllocated); + } else if (this.newExpenseCategoryInString != null) { + throw new CommandException(Messages.MESSAGE_INVALID_EXPENSE_CATEGORY); + } + + if (newExpenseName != null) { + if (newExpenseName.stripTrailing().isEmpty()) { + throw new CommandException(Messages.MESSAGE_INVALID_EXPENSE_NAME); + } + generatorToEdit.setExpenseName(newExpenseName); + } + + if (newExpenseAmount != null) { + if (!Price.isValidPrice(String.valueOf(newExpenseAmount))) { + throw new CommandException(Price.MESSAGE_CONSTRAINTS); + } + generatorToEdit.setAmount(newExpenseAmount); + } + + if (newExpenseEndDate != null) { + generatorToEdit.setEndDate(newExpenseEndDate); + } + + if (newFrequencyInString != null) { + // Check if it belongs in the enum + try { + RecurringExpenseType newTypeToSet = RecurringExpenseType.valueOf(newFrequencyInString); + generatorToEdit.setRecurringExpenseType(newTypeToSet); + } catch (IllegalArgumentException iae) { + throw new CommandException(Messages.MESSAGE_INVALID_ENUM_FOR_FREQUENCY); + } + } + return new CommandResult( + String.format(Messages.MESSAGE_SUCCESSFULLY_EDITED_RECURRING, generatorToEdit), + ScreenType.RECURRING_EXPENSE_SCREEN); + } + + @Override + public boolean equals(Object other) { + EditRecurringExpenseManagerCommand otherTypeCasted = (EditRecurringExpenseManagerCommand) other; + return targetIndex.equals(otherTypeCasted.targetIndex) + && (Objects.equals(newExpenseName, otherTypeCasted.newExpenseName)) + && (Objects.equals(newExpenseAmount, otherTypeCasted.newExpenseAmount)) + && (Objects.equals(newExpenseEndDate, otherTypeCasted.newExpenseEndDate)) + && (Objects.equals(newExpenseCategoryInString, otherTypeCasted.newExpenseCategoryInString)) + && (Objects.equals(newFrequencyInString, otherTypeCasted.newFrequencyInString)); + } +} diff --git a/src/main/java/seedu/address/logic/commands/exceptions/CommandException.java b/src/main/java/fasttrack/logic/commands/exceptions/CommandException.java similarity index 89% rename from src/main/java/seedu/address/logic/commands/exceptions/CommandException.java rename to src/main/java/fasttrack/logic/commands/exceptions/CommandException.java index a16bd14f2cd..9f71527929f 100644 --- a/src/main/java/seedu/address/logic/commands/exceptions/CommandException.java +++ b/src/main/java/fasttrack/logic/commands/exceptions/CommandException.java @@ -1,4 +1,4 @@ -package seedu.address.logic.commands.exceptions; +package fasttrack.logic.commands.exceptions; /** * Represents an error which occurs during execution of a {@link Command}. diff --git a/src/main/java/fasttrack/logic/commands/general/CategorySummaryCommand.java b/src/main/java/fasttrack/logic/commands/general/CategorySummaryCommand.java new file mode 100644 index 00000000000..0b37a27f7e5 --- /dev/null +++ b/src/main/java/fasttrack/logic/commands/general/CategorySummaryCommand.java @@ -0,0 +1,56 @@ +package fasttrack.logic.commands.general; + +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import fasttrack.commons.core.Messages; +import fasttrack.commons.core.index.Index; +import fasttrack.logic.commands.CommandResult; +import fasttrack.logic.commands.exceptions.CommandException; +import fasttrack.model.Model; +import fasttrack.model.category.Category; +import fasttrack.ui.ScreenType; + +/** + * Displays the summary of an expense + */ +public class CategorySummaryCommand implements GeneralCommand { + + public static final String COMMAND_WORD = "sumcat"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Displays summary of category " + + "Parameters: INDEX (must be a positive integer)\n" + + "Example: " + COMMAND_WORD + " 1"; + public static final String MESSAGE_SUCCESS = ""; + + private final Index targetIndex; + + /** + * Creates an CategorySummaryCommand to display summary of category at the specified {@code Index} + */ + public CategorySummaryCommand(Index index) { + requireNonNull(index); + targetIndex = index; + } + + @Override + public CommandResult execute(Model dataModel) throws CommandException { + requireNonNull(dataModel); + List lastShownList = dataModel.getFilteredCategoryList(); + + if (targetIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_CATEGORY_DISPLAYED_INDEX); + } + + Category targetCategory = lastShownList.get(targetIndex.getZeroBased()); + String toDisplay = targetCategory.getCategoryName() + " summary:\n" + targetCategory.getSummary(); + return new CommandResult(toDisplay, ScreenType.CATEGORY_SCREEN); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof CategorySummaryCommand // instanceof handles nulls + && targetIndex.equals(((CategorySummaryCommand) other).targetIndex)); // state check + } +} diff --git a/src/main/java/fasttrack/logic/commands/general/ClearCommand.java b/src/main/java/fasttrack/logic/commands/general/ClearCommand.java new file mode 100644 index 00000000000..998e536c7ca --- /dev/null +++ b/src/main/java/fasttrack/logic/commands/general/ClearCommand.java @@ -0,0 +1,25 @@ +package fasttrack.logic.commands.general; + +import fasttrack.logic.commands.CommandResult; +import fasttrack.model.Model; +import fasttrack.ui.ScreenType; + +/** + * Format full help instructions for every command for display. + */ +public class ClearCommand implements GeneralCommand { + + public static final String COMMAND_WORD = "CLEAR"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Deletes all entries in the FastTrack database.\n" + + "Example: " + COMMAND_WORD; + + public static final String MESSAGE_SUCCESS = "Deleted all prior entries."; + @Override + public CommandResult execute(Model dataModel) { + dataModel.clearCategory(); + dataModel.clearExpense(); + dataModel.clearRecurringExpenseGenerator(); + return new CommandResult(MESSAGE_SUCCESS, ScreenType.EXPENSE_SCREEN); + } +} diff --git a/src/main/java/fasttrack/logic/commands/general/ExitCommand.java b/src/main/java/fasttrack/logic/commands/general/ExitCommand.java new file mode 100644 index 00000000000..a30032d3eca --- /dev/null +++ b/src/main/java/fasttrack/logic/commands/general/ExitCommand.java @@ -0,0 +1,21 @@ +package fasttrack.logic.commands.general; + +import fasttrack.logic.commands.CommandResult; +import fasttrack.model.Model; +import fasttrack.ui.ScreenType; + +/** + * Terminates the program. + */ +public class ExitCommand implements GeneralCommand { + + public static final String COMMAND_WORD = "exit"; + + public static final String MESSAGE_EXIT_ACKNOWLEDGEMENT = "Exiting Expense Tracker as requested ..."; + + @Override + public CommandResult execute(Model dataModel) { + return new CommandResult(MESSAGE_EXIT_ACKNOWLEDGEMENT, false, true, ScreenType.EXPENSE_SCREEN); + } + +} diff --git a/src/main/java/fasttrack/logic/commands/general/FindCommand.java b/src/main/java/fasttrack/logic/commands/general/FindCommand.java new file mode 100644 index 00000000000..4d4bda30831 --- /dev/null +++ b/src/main/java/fasttrack/logic/commands/general/FindCommand.java @@ -0,0 +1,45 @@ +package fasttrack.logic.commands.general; + +import static java.util.Objects.requireNonNull; + +import fasttrack.commons.core.Messages; +import fasttrack.logic.commands.CommandResult; +import fasttrack.model.Model; +import fasttrack.model.expense.ExpenseContainsKeywordsPredicate; +import fasttrack.ui.ScreenType; + +/** + * Finds and lists all expenses in the expense tracker whose name contains any of the argument keywords. + * Keyword matching is case insensitive. + */ +public class FindCommand implements GeneralCommand { + + public static final String COMMAND_WORD = "find"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all expense which names contain any of " + + "the specified keywords (case-insensitive) and displays them as a list with index numbers.\n" + + "Parameters: KEYWORD [MORE_KEYWORDS]...\n" + + "Example: " + COMMAND_WORD + " apple orange cherry"; + + private final ExpenseContainsKeywordsPredicate predicate; + + public FindCommand(ExpenseContainsKeywordsPredicate predicate) { + this.predicate = predicate; + } + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + model.updateFilteredExpensesList(predicate); + return new CommandResult( + String.format(Messages.MESSAGE_EXPENSES_LISTED_OVERVIEW, + model.getFilteredExpenseList().size()), ScreenType.EXPENSE_SCREEN); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof FindCommand // instanceof handles nulls + && predicate.equals(((FindCommand) other).predicate)); // state check + } +} diff --git a/src/main/java/fasttrack/logic/commands/general/GeneralCommand.java b/src/main/java/fasttrack/logic/commands/general/GeneralCommand.java new file mode 100644 index 00000000000..324c71abbfb --- /dev/null +++ b/src/main/java/fasttrack/logic/commands/general/GeneralCommand.java @@ -0,0 +1,10 @@ +package fasttrack.logic.commands.general; + +import fasttrack.logic.commands.Command; + +/** + * Represents a general command with hidden internal logic and the ability to be + * executed. + */ +public interface GeneralCommand extends Command { +} diff --git a/src/main/java/seedu/address/logic/commands/HelpCommand.java b/src/main/java/fasttrack/logic/commands/general/HelpCommand.java similarity index 60% rename from src/main/java/seedu/address/logic/commands/HelpCommand.java rename to src/main/java/fasttrack/logic/commands/general/HelpCommand.java index bf824f91bd0..f45ed5dc735 100644 --- a/src/main/java/seedu/address/logic/commands/HelpCommand.java +++ b/src/main/java/fasttrack/logic/commands/general/HelpCommand.java @@ -1,11 +1,13 @@ -package seedu.address.logic.commands; +package fasttrack.logic.commands.general; -import seedu.address.model.Model; +import fasttrack.logic.commands.CommandResult; +import fasttrack.model.Model; +import fasttrack.ui.ScreenType; /** * Format full help instructions for every command for display. */ -public class HelpCommand extends Command { +public class HelpCommand implements GeneralCommand { public static final String COMMAND_WORD = "help"; @@ -15,7 +17,7 @@ public class HelpCommand extends Command { public static final String SHOWING_HELP_MESSAGE = "Opened help window."; @Override - public CommandResult execute(Model model) { - return new CommandResult(SHOWING_HELP_MESSAGE, true, false); + public CommandResult execute(Model dataModel) { + return new CommandResult(SHOWING_HELP_MESSAGE, true, false, ScreenType.EXPENSE_SCREEN); } } diff --git a/src/main/java/fasttrack/logic/commands/list/ListCategoryCommand.java b/src/main/java/fasttrack/logic/commands/list/ListCategoryCommand.java new file mode 100644 index 00000000000..a05a8cdb390 --- /dev/null +++ b/src/main/java/fasttrack/logic/commands/list/ListCategoryCommand.java @@ -0,0 +1,26 @@ +package fasttrack.logic.commands.list; + +import static fasttrack.model.Model.PREDICATE_SHOW_ALL_EXPENSES; +import static java.util.Objects.requireNonNull; + +import fasttrack.logic.commands.CommandResult; +import fasttrack.model.Model; +import fasttrack.ui.ScreenType; + +/** + * Lists all categories in the expense tracker to the user. + */ +public class ListCategoryCommand implements ListCommand { + + public static final String COMMAND_WORD = "lcat"; + + public static final String MESSAGE_SUCCESS = "Listed all categories"; + + + @Override + public CommandResult execute(Model dataModel) { + requireNonNull(dataModel); + dataModel.updateFilteredExpensesList(PREDICATE_SHOW_ALL_EXPENSES); + return new CommandResult(MESSAGE_SUCCESS, ScreenType.CATEGORY_SCREEN); + } +} diff --git a/src/main/java/fasttrack/logic/commands/list/ListCommand.java b/src/main/java/fasttrack/logic/commands/list/ListCommand.java new file mode 100644 index 00000000000..231f3b3fd93 --- /dev/null +++ b/src/main/java/fasttrack/logic/commands/list/ListCommand.java @@ -0,0 +1,11 @@ +package fasttrack.logic.commands.list; + +import fasttrack.logic.commands.Command; + +/** + * Represents a list command with hidden internal logic and the ability to be + * executed. + */ +public interface ListCommand extends Command { + +} diff --git a/src/main/java/fasttrack/logic/commands/list/ListExpensesCommand.java b/src/main/java/fasttrack/logic/commands/list/ListExpensesCommand.java new file mode 100644 index 00000000000..739b45b1b5e --- /dev/null +++ b/src/main/java/fasttrack/logic/commands/list/ListExpensesCommand.java @@ -0,0 +1,83 @@ +package fasttrack.logic.commands.list; + +import static fasttrack.logic.parser.CliSyntax.PREFIX_CATEGORY; +import static fasttrack.logic.parser.CliSyntax.PREFIX_TIMESPAN; +import static fasttrack.model.Model.PREDICATE_SHOW_ALL_EXPENSES; +import static java.util.Objects.requireNonNull; + +import java.util.Optional; +import java.util.function.Predicate; + +import fasttrack.commons.core.Messages; +import fasttrack.logic.commands.CommandResult; +import fasttrack.logic.parser.ParserUtil; +import fasttrack.model.Model; +import fasttrack.model.expense.Expense; +import fasttrack.model.expense.ExpenseInCategoryPredicate; +import fasttrack.model.expense.ExpenseInTimespanPredicate; +import fasttrack.ui.ScreenType; + +/** + * List all the expenses in the expense tracker. + */ +public class ListExpensesCommand implements ListCommand { + + public static final String COMMAND_WORD = "list"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Lists expenses based on a filter" + + "Parameters: " + + PREFIX_CATEGORY + "CATEGORY " + + PREFIX_TIMESPAN + "TIMESPAN (Week, Month, Year)\n" + + "Example: " + COMMAND_WORD + " " + + PREFIX_CATEGORY + "Food " + + PREFIX_TIMESPAN + "week "; + + private final Optional categoryPredicate; + private final Optional timespanPredicate; + + /** + * Creates a ListCommand to list out {@code Expense} by given filters + * @param categoryPredicate Predicate to filter by category + * @param timespanPredicate Predicate to filter by recency + */ + public ListExpensesCommand(Optional categoryPredicate, + Optional timespanPredicate) { + this.categoryPredicate = categoryPredicate; + this.timespanPredicate = timespanPredicate; + } + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + categoryPredicate.ifPresentOrElse(filter -> model.updateCategoryFilter( + filter.getCategory()), () -> model.updateCategoryFilter(null)); + timespanPredicate.ifPresentOrElse(filter -> model.updateTimeSpanFilter( + filter.getTimespan()), () -> model.updateTimeSpanFilter(ParserUtil.Timespan.ALL)); + Predicate combinedPredicate = PREDICATE_SHOW_ALL_EXPENSES; + if (categoryPredicate.isPresent()) { + combinedPredicate = combinedPredicate.and(categoryPredicate.get()); + } + if (timespanPredicate.isPresent()) { + combinedPredicate = combinedPredicate.and(timespanPredicate.get()); + } + model.updateFilteredExpensesList(combinedPredicate); + return new CommandResult( + String.format(Messages.MESSAGE_EXPENSES_LISTED_OVERVIEW, model.getFilteredExpenseList().size()), + ScreenType.EXPENSE_SCREEN); + + } + + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof ListExpensesCommand // instanceof handles nulls + && categoryPredicate.equals(((ListExpensesCommand) other).categoryPredicate) + && timespanPredicate.equals(((ListExpensesCommand) other).timespanPredicate)); // state check + } + + @Override + public String toString() { + return COMMAND_WORD + " " + categoryPredicate + " " + timespanPredicate; + } +} diff --git a/src/main/java/fasttrack/logic/commands/list/ListRecurringExpensesCommand.java b/src/main/java/fasttrack/logic/commands/list/ListRecurringExpensesCommand.java new file mode 100644 index 00000000000..f58520ffbda --- /dev/null +++ b/src/main/java/fasttrack/logic/commands/list/ListRecurringExpensesCommand.java @@ -0,0 +1,26 @@ +package fasttrack.logic.commands.list; + +import static fasttrack.model.Model.PREDICATE_SHOW_ALL_EXPENSES; +import static java.util.Objects.requireNonNull; + +import fasttrack.logic.commands.CommandResult; +import fasttrack.model.Model; +import fasttrack.ui.ScreenType; + +/** + * Lists all recurring expenses in the FastTrack to the user. + */ +public class ListRecurringExpensesCommand implements ListCommand { + + public static final String COMMAND_WORD = "lrec"; + + public static final String MESSAGE_SUCCESS = "Listed all recurring expenses"; + + + @Override + public CommandResult execute(Model dataModel) { + requireNonNull(dataModel); + dataModel.updateFilteredExpensesList(PREDICATE_SHOW_ALL_EXPENSES); + return new CommandResult(MESSAGE_SUCCESS, ScreenType.RECURRING_EXPENSE_SCREEN); + } +} diff --git a/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java b/src/main/java/fasttrack/logic/parser/ArgumentMultimap.java similarity index 98% rename from src/main/java/seedu/address/logic/parser/ArgumentMultimap.java rename to src/main/java/fasttrack/logic/parser/ArgumentMultimap.java index 954c8e18f8e..55724582d9b 100644 --- a/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java +++ b/src/main/java/fasttrack/logic/parser/ArgumentMultimap.java @@ -1,4 +1,4 @@ -package seedu.address.logic.parser; +package fasttrack.logic.parser; import java.util.ArrayList; import java.util.HashMap; diff --git a/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java b/src/main/java/fasttrack/logic/parser/ArgumentTokenizer.java similarity index 99% rename from src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java rename to src/main/java/fasttrack/logic/parser/ArgumentTokenizer.java index 5c9aebfa488..0f1a1da1449 100644 --- a/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java +++ b/src/main/java/fasttrack/logic/parser/ArgumentTokenizer.java @@ -1,4 +1,4 @@ -package seedu.address.logic.parser; +package fasttrack.logic.parser; import java.util.ArrayList; import java.util.Arrays; diff --git a/src/main/java/fasttrack/logic/parser/CategorySummaryParser.java b/src/main/java/fasttrack/logic/parser/CategorySummaryParser.java new file mode 100644 index 00000000000..c072aa1907f --- /dev/null +++ b/src/main/java/fasttrack/logic/parser/CategorySummaryParser.java @@ -0,0 +1,29 @@ +package fasttrack.logic.parser; + +import static fasttrack.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import fasttrack.commons.core.index.Index; +import fasttrack.logic.commands.general.CategorySummaryCommand; +import fasttrack.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new CategorySummaryCommand object + */ +public class CategorySummaryParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the CategorySummaryCommand + * and returns a CategorySummaryCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public CategorySummaryCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new CategorySummaryCommand(index); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, CategorySummaryCommand.MESSAGE_USAGE), pe); + } + } + +} diff --git a/src/main/java/fasttrack/logic/parser/CliSyntax.java b/src/main/java/fasttrack/logic/parser/CliSyntax.java new file mode 100644 index 00000000000..bc15762d921 --- /dev/null +++ b/src/main/java/fasttrack/logic/parser/CliSyntax.java @@ -0,0 +1,19 @@ +package fasttrack.logic.parser; + + +/** + * Contains Command Line Interface (CLI) syntax definitions common to multiple commands + */ +public class CliSyntax { + + /* Prefix definitions */ + public static final Prefix PREFIX_NAME = new Prefix("n/"); + public static final Prefix PREFIX_CATEGORY = new Prefix("c/"); + public static final Prefix PREFIX_SUMMARY = new Prefix("s/"); + public static final Prefix PREFIX_PRICE = new Prefix("p/"); + public static final Prefix PREFIX_DATE = new Prefix("d/"); + public static final Prefix PREFIX_TIMESPAN = new Prefix("t/"); + public static final Prefix PREFIX_START_DATE = new Prefix("sd/"); + public static final Prefix PREFIX_END_DATE = new Prefix("ed/"); + +} diff --git a/src/main/java/fasttrack/logic/parser/ExpenseTrackerParser.java b/src/main/java/fasttrack/logic/parser/ExpenseTrackerParser.java new file mode 100644 index 00000000000..7f892896cb3 --- /dev/null +++ b/src/main/java/fasttrack/logic/parser/ExpenseTrackerParser.java @@ -0,0 +1,126 @@ +package fasttrack.logic.parser; + +import static fasttrack.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static fasttrack.commons.core.Messages.MESSAGE_UNKNOWN_COMMAND; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import fasttrack.logic.commands.Command; +import fasttrack.logic.commands.SetBudgetCommand; +import fasttrack.logic.commands.add.AddCategoryCommand; +import fasttrack.logic.commands.add.AddExpenseCommand; +import fasttrack.logic.commands.add.AddRecurringExpenseCommand; +import fasttrack.logic.commands.delete.DeleteCategoryCommand; +import fasttrack.logic.commands.delete.DeleteExpenseCommand; +import fasttrack.logic.commands.delete.DeleteRecurringExpenseCommand; +import fasttrack.logic.commands.edit.EditCategoryCommand; +import fasttrack.logic.commands.edit.EditExpenseCommand; +import fasttrack.logic.commands.edit.EditRecurringExpenseManagerCommand; +import fasttrack.logic.commands.general.CategorySummaryCommand; +import fasttrack.logic.commands.general.ClearCommand; +import fasttrack.logic.commands.general.ExitCommand; +import fasttrack.logic.commands.general.FindCommand; +import fasttrack.logic.commands.general.HelpCommand; +import fasttrack.logic.commands.list.ListCategoryCommand; +import fasttrack.logic.commands.list.ListExpensesCommand; +import fasttrack.logic.commands.list.ListRecurringExpensesCommand; +import fasttrack.logic.parser.add.AddCategoryCommandParser; +import fasttrack.logic.parser.add.AddExpenseCommandParser; +import fasttrack.logic.parser.add.AddRecurringExpenseCommandParser; +import fasttrack.logic.parser.delete.DeleteCategoryCommandParser; +import fasttrack.logic.parser.delete.DeleteExpenseCommandParser; +import fasttrack.logic.parser.delete.DeleteRecurringExpenseCommandParser; +import fasttrack.logic.parser.edit.EditCategoryCommandParser; +import fasttrack.logic.parser.edit.EditExpenseCommandParser; +import fasttrack.logic.parser.edit.EditRecurringExpenseManagerCommandParser; +import fasttrack.logic.parser.exceptions.ParseException; + +/** + * Parses user input. + */ +public class ExpenseTrackerParser { + + /** + * Used for initial separation of command word and args. + */ + private static final Pattern BASIC_COMMAND_FORMAT = Pattern.compile("(?\\S+)(?.*)"); + + /** + * Parses user input into command for execution. + * + * @param userInput full user input string + * @return the command based on the user input + * @throws ParseException if the user input does not conform the expected format + */ + public Command parseCommand(String userInput) throws ParseException { + final Matcher matcher = BASIC_COMMAND_FORMAT.matcher(userInput.trim()); + if (!matcher.matches()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, HelpCommand.MESSAGE_USAGE)); + } + + final String commandWord = matcher.group("commandWord"); + final String arguments = matcher.group("arguments"); + + switch (commandWord) { + + case AddExpenseCommand.COMMAND_WORD: + return new AddExpenseCommandParser().parse(arguments); + + case DeleteExpenseCommand.COMMAND_WORD: + return new DeleteExpenseCommandParser().parse(arguments); + + case ListExpensesCommand.COMMAND_WORD: + return new ListCommandParser().parse(arguments); + + case ListCategoryCommand.COMMAND_WORD: + return new ListCategoryCommand(); + + case ExitCommand.COMMAND_WORD: + return new ExitCommand(); + + case FindCommand.COMMAND_WORD: + return new FindCommandParser().parse(arguments); + + case HelpCommand.COMMAND_WORD: + return new HelpCommand(); + + case AddCategoryCommand.COMMAND_WORD: + return new AddCategoryCommandParser().parse(arguments); + + case DeleteCategoryCommand.COMMAND_WORD: + return new DeleteCategoryCommandParser().parse(arguments); + + case EditCategoryCommand.COMMAND_WORD: + return new EditCategoryCommandParser().parse(arguments); + + case EditExpenseCommand.COMMAND_WORD: + return new EditExpenseCommandParser().parse(arguments); + + case SetBudgetCommand.COMMAND_WORD: + return new SetBudgetParser().parse(arguments); + + case CategorySummaryCommand.COMMAND_WORD: + return new CategorySummaryParser().parse(arguments); + + case ClearCommand.COMMAND_WORD: + return new ClearCommand(); + + case EditRecurringExpenseManagerCommand.COMMAND_WORD: + return new EditRecurringExpenseManagerCommandParser().parse(arguments); + + case AddRecurringExpenseCommand.COMMAND_WORD: + return new AddRecurringExpenseCommandParser().parse(arguments); + + case DeleteRecurringExpenseCommand.COMMAND_WORD: + return new DeleteRecurringExpenseCommandParser().parse(arguments); + + case ListRecurringExpensesCommand.COMMAND_WORD: + return new ListRecurringExpensesCommand(); + + default: + throw new ParseException(MESSAGE_UNKNOWN_COMMAND); + } + } + +} diff --git a/src/main/java/seedu/address/logic/parser/FindCommandParser.java b/src/main/java/fasttrack/logic/parser/FindCommandParser.java similarity index 66% rename from src/main/java/seedu/address/logic/parser/FindCommandParser.java rename to src/main/java/fasttrack/logic/parser/FindCommandParser.java index 4fb71f23103..32e63459b2c 100644 --- a/src/main/java/seedu/address/logic/parser/FindCommandParser.java +++ b/src/main/java/fasttrack/logic/parser/FindCommandParser.java @@ -1,12 +1,12 @@ -package seedu.address.logic.parser; +package fasttrack.logic.parser; -import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static fasttrack.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; import java.util.Arrays; -import seedu.address.logic.commands.FindCommand; -import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.person.NameContainsKeywordsPredicate; +import fasttrack.logic.commands.general.FindCommand; +import fasttrack.logic.parser.exceptions.ParseException; +import fasttrack.model.expense.ExpenseContainsKeywordsPredicate; /** * Parses input arguments and creates a new FindCommand object @@ -27,7 +27,7 @@ public FindCommand parse(String args) throws ParseException { String[] nameKeywords = trimmedArgs.split("\\s+"); - return new FindCommand(new NameContainsKeywordsPredicate(Arrays.asList(nameKeywords))); + return new FindCommand(new ExpenseContainsKeywordsPredicate(Arrays.asList(nameKeywords))); } } diff --git a/src/main/java/fasttrack/logic/parser/ListCommandParser.java b/src/main/java/fasttrack/logic/parser/ListCommandParser.java new file mode 100644 index 00000000000..401cd905f73 --- /dev/null +++ b/src/main/java/fasttrack/logic/parser/ListCommandParser.java @@ -0,0 +1,56 @@ +package fasttrack.logic.parser; + + +import static fasttrack.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static fasttrack.logic.parser.CliSyntax.PREFIX_CATEGORY; +import static fasttrack.logic.parser.CliSyntax.PREFIX_TIMESPAN; + +import java.util.Optional; + +import fasttrack.logic.commands.list.ListExpensesCommand; +import fasttrack.logic.parser.exceptions.ParseException; +import fasttrack.model.expense.ExpenseInCategoryPredicate; +import fasttrack.model.expense.ExpenseInTimespanPredicate; + +/** + * Parses input arguments and creates a new AddCommand object + */ +public class ListCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the AddCommand + * and returns an AddCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public ListExpensesCommand parse(String args) throws ParseException { + + if (args.equals("")) { + return new ListExpensesCommand(Optional.empty(), Optional.empty()); + } + + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_CATEGORY, PREFIX_TIMESPAN); + + if (!argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, ListExpensesCommand.MESSAGE_USAGE)); + } + + Optional categoryStringArg = argMultimap.getValue(PREFIX_CATEGORY); + Optional timespanStringArg = argMultimap.getValue(PREFIX_TIMESPAN); + + ExpenseInCategoryPredicate categoryPredicate = null; + ExpenseInTimespanPredicate timespanPredicate = null; + + if (categoryStringArg.isPresent()) { + categoryPredicate = new ExpenseInCategoryPredicate(ParserUtil.parseCategory(categoryStringArg.get())); + } + if (timespanStringArg.isPresent()) { + timespanPredicate = new ExpenseInTimespanPredicate(ParserUtil.parseTimespan(timespanStringArg.get())); + } + + + return new ListExpensesCommand( + Optional.ofNullable(categoryPredicate), + Optional.ofNullable(timespanPredicate)); + } +} diff --git a/src/main/java/seedu/address/logic/parser/Parser.java b/src/main/java/fasttrack/logic/parser/Parser.java similarity index 72% rename from src/main/java/seedu/address/logic/parser/Parser.java rename to src/main/java/fasttrack/logic/parser/Parser.java index d6551ad8e3f..997322e0381 100644 --- a/src/main/java/seedu/address/logic/parser/Parser.java +++ b/src/main/java/fasttrack/logic/parser/Parser.java @@ -1,7 +1,7 @@ -package seedu.address.logic.parser; +package fasttrack.logic.parser; -import seedu.address.logic.commands.Command; -import seedu.address.logic.parser.exceptions.ParseException; +import fasttrack.logic.commands.Command; +import fasttrack.logic.parser.exceptions.ParseException; /** * Represents a Parser that is able to parse user input into a {@code Command} of type {@code T}. diff --git a/src/main/java/fasttrack/logic/parser/ParserUtil.java b/src/main/java/fasttrack/logic/parser/ParserUtil.java new file mode 100644 index 00000000000..8f82a910a7a --- /dev/null +++ b/src/main/java/fasttrack/logic/parser/ParserUtil.java @@ -0,0 +1,199 @@ +package fasttrack.logic.parser; + +import static fasttrack.commons.core.Messages.MESSAGE_INVALID_DATE_FORMAT; +import static java.util.Objects.requireNonNull; + +import java.time.DayOfWeek; +import java.time.LocalDate; + +import fasttrack.commons.core.index.Index; +import fasttrack.commons.util.StringUtil; +import fasttrack.logic.parser.exceptions.ParseException; +import fasttrack.model.category.Category; +import fasttrack.model.category.MiscellaneousCategory; +import fasttrack.model.category.UserDefinedCategory; +import fasttrack.model.expense.Price; +import fasttrack.model.expense.RecurringExpenseType; +import fasttrack.model.util.CommandUtility; + +/** + * Contains utility methods used for parsing strings in the various *Parser classes. + */ +public class ParserUtil { + + /** + * Enumerates possibilities for timespans indicated: week, month, year. + */ + public enum Timespan { + WEEK("Week"), + MONTH("Month"), + YEAR("Year"), + ALL("All"); + + private final String stringRep; + Timespan(String stringRep) { + this.stringRep = stringRep; + } + // Return the string representation of the given timeSpan + @Override + public String toString() { + return stringRep; + } + } + + + public static final String MESSAGE_INVALID_INDEX = "The index provided is invalid."; + + /** + * Parses {@code oneBasedIndex} into an {@code Index} and returns it. + * Leading and trailing whitespaces will be + * trimmed. + * @throws ParseException if the specified index is invalid (not non-zero unsigned integer). + */ + public static Index parseIndex(String oneBasedIndex) throws ParseException { + String trimmedIndex = oneBasedIndex.trim(); + if (!StringUtil.isNonZeroUnsignedInteger(trimmedIndex)) { + throw new ParseException(MESSAGE_INVALID_INDEX); + } + return Index.fromOneBased(Integer.parseInt(trimmedIndex)); + } + + /** + * Parses a {@code String expenseName} into a String. + * Leading and trailing whitespaces will be trimmed. + * @throws ParseException if the given {@code expenseName} is invalid. + */ + public static String parseExpenseName(String expenseName) throws ParseException { + requireNonNull(expenseName); + String trimmedName = expenseName.trim(); + if (trimmedName.isEmpty()) { + throw new ParseException("The expense name should not be empty!"); + } + return trimmedName; + } + + /** + * Parses {@code price} into an {@code Price} and returns it. + * Leading and trailing whitespaces will be trimmed. + * @throws ParseException if the specified price is invalid (not non-negative and numeric). + */ + public static Price parsePrice(String price) throws ParseException { + requireNonNull(price); + String trimmedPrice = price.trim(); + try { + if (Price.isValidPrice(trimmedPrice)) { + return new Price(trimmedPrice); + } else { + throw new ParseException(Price.MESSAGE_CONSTRAINTS); + } + } catch (NumberFormatException e) { + throw new ParseException(Price.MESSAGE_CONSTRAINTS); + } + } + + /** + * Parses {@code categoryName} and creates a {@code Category} instance and returns it. + * Leading and trailing whitespaces will be + * trimmed. + * @throws ParseException if the specified categoryName does not exist + */ + public static Category parseCategory(String categoryName) throws ParseException { + String trimmedCategoryName = categoryName.trim(); + if (trimmedCategoryName.isEmpty() || !Category.isValidCategoryName(trimmedCategoryName)) { + throw new ParseException(Category.MESSAGE_CONSTRAINTS); + } + if (trimmedCategoryName.equals("miscellaneous")) { + return new MiscellaneousCategory(); + } + return new UserDefinedCategory(trimmedCategoryName, ""); + } + + /** + * Parses a {@code String category} into a {@code UserDefinedCategory}. + * Leading and trailing whitespaces will be trimmed. + * @throws ParseException if the given {@code category} is invalid. + */ + public static UserDefinedCategory parseCategory(String category, String summary) throws ParseException { + requireNonNull(category); + String trimmedCategory = category.trim(); + if (!Category.isValidCategoryName(trimmedCategory)) { + throw new ParseException(Category.MESSAGE_CONSTRAINTS); + } + return new UserDefinedCategory(trimmedCategory, summary); + } + + /** + * Parses {@code dateString} into a {@code LocalDate} instance and returns it. + * @throws ParseException if the date could not be parsed + */ + public static LocalDate parseDate(String dateString) throws ParseException { + String trimmedDate = dateString.trim(); + LocalDate parsedDate; + try { + parsedDate = CommandUtility.parseDateFromUserInput(trimmedDate); + } catch (IllegalArgumentException e) { + throw new ParseException(MESSAGE_INVALID_DATE_FORMAT); + } + return parsedDate; + } + /** + * Parses {@code String timespan} into a {@code LocalDate}. + */ + public static Timespan parseTimespan(String timespan) throws ParseException { + assert timespan != null : "input should not be null"; + requireNonNull(timespan); + String trimmedTimespan = timespan.trim(); + if (trimmedTimespan.equalsIgnoreCase("week") || trimmedTimespan.equalsIgnoreCase("w")) { + return Timespan.WEEK; + } + if (trimmedTimespan.equalsIgnoreCase("month") || trimmedTimespan.equalsIgnoreCase("m")) { + return Timespan.MONTH; + } + if (trimmedTimespan.equalsIgnoreCase("year") || trimmedTimespan.equalsIgnoreCase("y")) { + return Timespan.YEAR; + } + throw new ParseException("Not a valid date format (week, month, year)"); + } + + /** + * Parses {@code String timespan} into a {@code RecurringExpenseType}. + */ + public static RecurringExpenseType parseTimeSpanRecurringExpense(String timespan) throws ParseException { + assert timespan != null : "input should not be null"; + requireNonNull(timespan); + String trimmedTimespan = timespan.trim(); + if (trimmedTimespan.equalsIgnoreCase("day") || trimmedTimespan.equalsIgnoreCase("d")) { + return RecurringExpenseType.DAILY; + } + if (trimmedTimespan.equalsIgnoreCase("week") || trimmedTimespan.equalsIgnoreCase("w")) { + return RecurringExpenseType.WEEKLY; + } + if (trimmedTimespan.equalsIgnoreCase("month") || trimmedTimespan.equalsIgnoreCase("m")) { + return RecurringExpenseType.MONTHLY; + } + if (trimmedTimespan.equalsIgnoreCase("year") || trimmedTimespan.equalsIgnoreCase("y")) { + return RecurringExpenseType.YEARLY; + } + throw new ParseException("Not a valid date format (day, week, month, year)"); + } + + /** + * Get earliest {@code LocalDate} within the given {@code Timespan}. + * @param t {@code Timespan} of week, month or year + * @return LocalDate of the earliest date in this timespan + */ + public static LocalDate getDateByTimespan(Timespan t) { + switch (t) { + case WEEK: + return LocalDate.now().with(DayOfWeek.MONDAY); + case MONTH: + return LocalDate.now().withDayOfMonth(1); + case YEAR: + return LocalDate.now().withDayOfYear(1); + default: + break; + } + assert false : "Line should not be reached"; + return null; + } +} diff --git a/src/main/java/seedu/address/logic/parser/Prefix.java b/src/main/java/fasttrack/logic/parser/Prefix.java similarity index 95% rename from src/main/java/seedu/address/logic/parser/Prefix.java rename to src/main/java/fasttrack/logic/parser/Prefix.java index c859d5fa5db..d5ed018f6b0 100644 --- a/src/main/java/seedu/address/logic/parser/Prefix.java +++ b/src/main/java/fasttrack/logic/parser/Prefix.java @@ -1,4 +1,4 @@ -package seedu.address.logic.parser; +package fasttrack.logic.parser; /** * A prefix that marks the beginning of an argument in an arguments string. diff --git a/src/main/java/fasttrack/logic/parser/SetBudgetParser.java b/src/main/java/fasttrack/logic/parser/SetBudgetParser.java new file mode 100644 index 00000000000..4bcbff4eb15 --- /dev/null +++ b/src/main/java/fasttrack/logic/parser/SetBudgetParser.java @@ -0,0 +1,46 @@ +package fasttrack.logic.parser; + +import static fasttrack.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static fasttrack.logic.parser.CliSyntax.PREFIX_PRICE; + +import java.util.stream.Stream; + +import fasttrack.logic.commands.SetBudgetCommand; +import fasttrack.logic.parser.exceptions.ParseException; +import fasttrack.model.Budget; +import fasttrack.model.expense.Price; + +/** + * Parses input arguments and creates a new AddExpenseCommand object + */ +public class SetBudgetParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the AddExpenseCommand + * and returns an AddExpenseCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public SetBudgetCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_PRICE); + + if (!arePrefixesPresent(argMultimap, PREFIX_PRICE) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, SetBudgetCommand.MESSAGE_USAGE)); + } + + Price amount = ParserUtil.parsePrice(argMultimap.getValue(PREFIX_PRICE).get()); + Budget budget = new Budget(amount.getPriceAsDouble()); + + return new SetBudgetCommand(budget); + } + + /** + * Returns true if none of the prefixes contains empty {@code Optional} values in the given + * {@code ArgumentMultimap}. + */ + private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } + +} diff --git a/src/main/java/fasttrack/logic/parser/add/AddCategoryCommandParser.java b/src/main/java/fasttrack/logic/parser/add/AddCategoryCommandParser.java new file mode 100644 index 00000000000..29c11dfe062 --- /dev/null +++ b/src/main/java/fasttrack/logic/parser/add/AddCategoryCommandParser.java @@ -0,0 +1,54 @@ +package fasttrack.logic.parser.add; + +import static fasttrack.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static fasttrack.logic.parser.CliSyntax.PREFIX_CATEGORY; +import static fasttrack.logic.parser.CliSyntax.PREFIX_SUMMARY; + +import java.util.stream.Stream; + +import fasttrack.logic.commands.add.AddCategoryCommand; +import fasttrack.logic.parser.ArgumentMultimap; +import fasttrack.logic.parser.ArgumentTokenizer; +import fasttrack.logic.parser.Parser; +import fasttrack.logic.parser.ParserUtil; +import fasttrack.logic.parser.Prefix; +import fasttrack.logic.parser.exceptions.ParseException; +import fasttrack.model.category.Category; + +/** + * Parses input arguments and creates a new AddCategoryCommand object + */ +public class AddCategoryCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the AddCommand + * and returns an AddCategoryCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public AddCategoryCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_CATEGORY, PREFIX_SUMMARY); + + if (!arePrefixesPresent(argMultimap, PREFIX_CATEGORY) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCategoryCommand.MESSAGE_USAGE)); + } + String summary = ""; + if (arePrefixesPresent(argMultimap, PREFIX_SUMMARY)) { + summary = argMultimap.getValue(PREFIX_SUMMARY).get(); + } + + Category category = ParserUtil.parseCategory(argMultimap.getValue(PREFIX_CATEGORY).get(), summary); + + return new AddCategoryCommand(category); + } + + /** + * Returns true if none of the prefixes contains empty {@code Optional} values in the given + * {@code ArgumentMultimap}. + */ + private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } + +} diff --git a/src/main/java/fasttrack/logic/parser/add/AddExpenseCommandParser.java b/src/main/java/fasttrack/logic/parser/add/AddExpenseCommandParser.java new file mode 100644 index 00000000000..be2cb234a11 --- /dev/null +++ b/src/main/java/fasttrack/logic/parser/add/AddExpenseCommandParser.java @@ -0,0 +1,66 @@ +package fasttrack.logic.parser.add; + +import static fasttrack.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static fasttrack.logic.parser.CliSyntax.PREFIX_CATEGORY; +import static fasttrack.logic.parser.CliSyntax.PREFIX_DATE; +import static fasttrack.logic.parser.CliSyntax.PREFIX_NAME; +import static fasttrack.logic.parser.CliSyntax.PREFIX_PRICE; + +import java.time.LocalDate; +import java.util.Optional; +import java.util.stream.Stream; + +import fasttrack.logic.commands.add.AddExpenseCommand; +import fasttrack.logic.parser.ArgumentMultimap; +import fasttrack.logic.parser.ArgumentTokenizer; +import fasttrack.logic.parser.Parser; +import fasttrack.logic.parser.ParserUtil; +import fasttrack.logic.parser.Prefix; +import fasttrack.logic.parser.exceptions.ParseException; +import fasttrack.model.category.Category; +import fasttrack.model.expense.Expense; +import fasttrack.model.expense.Price; + +/** + * Parses input arguments and creates a new AddExpenseCommand object + */ +public class AddExpenseCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the AddExpenseCommand + * and returns an AddExpenseCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public AddExpenseCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_CATEGORY, PREFIX_PRICE, PREFIX_DATE); + + if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_CATEGORY, PREFIX_PRICE) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddExpenseCommand.MESSAGE_USAGE)); + } + + String name = ParserUtil.parseExpenseName(argMultimap.getValue(PREFIX_NAME).get()); + Category category = ParserUtil.parseCategory(argMultimap.getValue(PREFIX_CATEGORY).get()); + Price amount = ParserUtil.parsePrice(argMultimap.getValue(PREFIX_PRICE).get()); + + Optional dateStringArg = argMultimap.getValue(PREFIX_DATE); + + LocalDate date = LocalDate.now(); + if (dateStringArg.isPresent()) { + date = ParserUtil.parseDate(dateStringArg.get()); + } + + Expense expenseToAdd = new Expense(name, amount, date, category); + return new AddExpenseCommand(expenseToAdd); + } + + /** + * Returns true if none of the prefixes contains empty {@code Optional} values in the given + * {@code ArgumentMultimap}. + */ + private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } + +} diff --git a/src/main/java/fasttrack/logic/parser/add/AddRecurringExpenseCommandParser.java b/src/main/java/fasttrack/logic/parser/add/AddRecurringExpenseCommandParser.java new file mode 100644 index 00000000000..bb42f842bf2 --- /dev/null +++ b/src/main/java/fasttrack/logic/parser/add/AddRecurringExpenseCommandParser.java @@ -0,0 +1,78 @@ +package fasttrack.logic.parser.add; + +import static fasttrack.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static fasttrack.logic.parser.CliSyntax.PREFIX_CATEGORY; +import static fasttrack.logic.parser.CliSyntax.PREFIX_END_DATE; +import static fasttrack.logic.parser.CliSyntax.PREFIX_NAME; +import static fasttrack.logic.parser.CliSyntax.PREFIX_PRICE; +import static fasttrack.logic.parser.CliSyntax.PREFIX_START_DATE; +import static fasttrack.logic.parser.CliSyntax.PREFIX_TIMESPAN; + +import java.time.LocalDate; +import java.util.stream.Stream; + +import fasttrack.logic.commands.add.AddRecurringExpenseCommand; +import fasttrack.logic.parser.ArgumentMultimap; +import fasttrack.logic.parser.ArgumentTokenizer; +import fasttrack.logic.parser.Parser; +import fasttrack.logic.parser.ParserUtil; +import fasttrack.logic.parser.Prefix; +import fasttrack.logic.parser.exceptions.ParseException; +import fasttrack.model.category.Category; +import fasttrack.model.expense.Price; +import fasttrack.model.expense.RecurringExpenseManager; +import fasttrack.model.expense.RecurringExpenseType; + +/** + * Parses input arguments and creates a new AddCategoryCommand object + */ +public class AddRecurringExpenseCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the AddCommand + * and returns an AddCategoryCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public AddRecurringExpenseCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_CATEGORY, PREFIX_PRICE , PREFIX_START_DATE, + PREFIX_END_DATE, PREFIX_TIMESPAN); + + if (!arePrefixesPresent(argMultimap, PREFIX_CATEGORY, PREFIX_START_DATE, PREFIX_PRICE, PREFIX_TIMESPAN, + PREFIX_NAME) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + AddRecurringExpenseCommand.MESSAGE_USAGE)); + } + + Category category = ParserUtil.parseCategory(argMultimap.getValue(PREFIX_CATEGORY).get()); + Price price = ParserUtil.parsePrice(argMultimap.getValue(PREFIX_PRICE).get()); + String name = ParserUtil.parseExpenseName(argMultimap.getValue(PREFIX_NAME).get()); + LocalDate startDate = ParserUtil.parseDate(argMultimap.getValue(PREFIX_START_DATE).get()); + RecurringExpenseType timespan = ParserUtil.parseTimeSpanRecurringExpense( + argMultimap.getValue(PREFIX_TIMESPAN).get()); + LocalDate endDate = null; + if (arePrefixesPresent(argMultimap, PREFIX_END_DATE)) { + endDate = ParserUtil.parseDate(argMultimap.getValue(PREFIX_END_DATE).get()); + if (endDate.isBefore(startDate)) { + throw new ParseException("End date provided is earlier than start date."); + } + RecurringExpenseManager toAdd = new RecurringExpenseManager(name, price, + category, startDate, endDate, timespan); + return new AddRecurringExpenseCommand(toAdd); + } + + RecurringExpenseManager toAdd = new RecurringExpenseManager(name, price, + category, startDate, timespan); + return new AddRecurringExpenseCommand(toAdd); + } + + /** + * Returns true if none of the prefixes contains empty {@code Optional} values in the given + * {@code ArgumentMultimap}. + */ + private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } + +} diff --git a/src/main/java/fasttrack/logic/parser/delete/DeleteCategoryCommandParser.java b/src/main/java/fasttrack/logic/parser/delete/DeleteCategoryCommandParser.java new file mode 100644 index 00000000000..c8b1337702f --- /dev/null +++ b/src/main/java/fasttrack/logic/parser/delete/DeleteCategoryCommandParser.java @@ -0,0 +1,31 @@ +package fasttrack.logic.parser.delete; + +import static fasttrack.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import fasttrack.commons.core.index.Index; +import fasttrack.logic.commands.delete.DeleteCategoryCommand; +import fasttrack.logic.parser.Parser; +import fasttrack.logic.parser.ParserUtil; +import fasttrack.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new DeleteCommand object + */ +public class DeleteCategoryCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the DeleteCommand + * and returns a DeleteCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public DeleteCategoryCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new DeleteCategoryCommand(index); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCategoryCommand.MESSAGE_USAGE), pe); + } + } + +} diff --git a/src/main/java/fasttrack/logic/parser/delete/DeleteExpenseCommandParser.java b/src/main/java/fasttrack/logic/parser/delete/DeleteExpenseCommandParser.java new file mode 100644 index 00000000000..82aaaefadb8 --- /dev/null +++ b/src/main/java/fasttrack/logic/parser/delete/DeleteExpenseCommandParser.java @@ -0,0 +1,31 @@ +package fasttrack.logic.parser.delete; + +import static fasttrack.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import fasttrack.commons.core.index.Index; +import fasttrack.logic.commands.delete.DeleteExpenseCommand; +import fasttrack.logic.parser.Parser; +import fasttrack.logic.parser.ParserUtil; +import fasttrack.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new DeleteExpenseCommandParser object + */ +public class DeleteExpenseCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the DeleteExpenseCommandParser + * and returns a DeleteExpenseCommandParser object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public DeleteExpenseCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new DeleteExpenseCommand(index); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteExpenseCommand.MESSAGE_USAGE), pe); + } + } + +} diff --git a/src/main/java/fasttrack/logic/parser/delete/DeleteRecurringExpenseCommandParser.java b/src/main/java/fasttrack/logic/parser/delete/DeleteRecurringExpenseCommandParser.java new file mode 100644 index 00000000000..f80afd3b570 --- /dev/null +++ b/src/main/java/fasttrack/logic/parser/delete/DeleteRecurringExpenseCommandParser.java @@ -0,0 +1,31 @@ +package fasttrack.logic.parser.delete; + +import static fasttrack.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import fasttrack.commons.core.index.Index; +import fasttrack.logic.commands.delete.DeleteRecurringExpenseCommand; +import fasttrack.logic.parser.Parser; +import fasttrack.logic.parser.ParserUtil; +import fasttrack.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new DeleteExpenseCommandParser object + */ +public class DeleteRecurringExpenseCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the DeleteExpenseCommandParser + * and returns a DeleteExpenseCommandParser object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public DeleteRecurringExpenseCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new DeleteRecurringExpenseCommand(index); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteRecurringExpenseCommand.MESSAGE_USAGE), pe); + } + } + +} diff --git a/src/main/java/fasttrack/logic/parser/edit/EditCategoryCommandParser.java b/src/main/java/fasttrack/logic/parser/edit/EditCategoryCommandParser.java new file mode 100644 index 00000000000..e41654c4fec --- /dev/null +++ b/src/main/java/fasttrack/logic/parser/edit/EditCategoryCommandParser.java @@ -0,0 +1,61 @@ +package fasttrack.logic.parser.edit; + +import static fasttrack.logic.parser.CliSyntax.PREFIX_CATEGORY; +import static fasttrack.logic.parser.CliSyntax.PREFIX_SUMMARY; + +import fasttrack.commons.core.index.Index; +import fasttrack.logic.commands.edit.EditCategoryCommand; +import fasttrack.logic.parser.ArgumentMultimap; +import fasttrack.logic.parser.ArgumentTokenizer; +import fasttrack.logic.parser.Parser; +import fasttrack.logic.parser.ParserUtil; +import fasttrack.logic.parser.Prefix; +import fasttrack.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new EditCategory object + */ +public class EditCategoryCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the EditCategory + * @param args Arguments provided by user in String form. + * @return an instance of EditCategory with the necessary arguments extracted from user's arguments. + * @throws ParseException if the user input does not conform to required format. + */ + public EditCategoryCommand parse(String args) throws ParseException { + //First check if the given index is valid. + ArgumentMultimap argMultiMap = + ArgumentTokenizer.tokenize(args, PREFIX_CATEGORY, PREFIX_SUMMARY); + Index index = ParserUtil.parseIndex(argMultiMap.getPreamble()); + + //Get the new category name & summary if applicable + if (isPrefixPresent(argMultiMap, PREFIX_CATEGORY) && isPrefixPresent(argMultiMap, PREFIX_SUMMARY)) { + String newSummary = argMultiMap.getValue(PREFIX_SUMMARY).get(); + String newCategoryName = argMultiMap.getValue(PREFIX_CATEGORY).get(); + return new EditCategoryCommand(index, newCategoryName, newSummary); + } + + if (isPrefixPresent(argMultiMap, PREFIX_CATEGORY)) { + String newCategoryName = argMultiMap.getValue(PREFIX_CATEGORY).get(); + return new EditCategoryCommand(index, newCategoryName, null); + } + + if (isPrefixPresent(argMultiMap, PREFIX_SUMMARY)) { + String newSummary = argMultiMap.getValue(PREFIX_SUMMARY).get(); + return new EditCategoryCommand(index, null, newSummary); + } + + return new EditCategoryCommand(index, null, null); + } + + /** + * Returns true if the given prefix does not contain {@code Optional} values in the given {@code ArgumentMultimap} + * @param argMultiMap The argument multimap to check for. + * @param toCheck The prefix to check for. + * @return Boolean that indicates whether the given prefix is present or not. + */ + private static boolean isPrefixPresent(ArgumentMultimap argMultiMap, Prefix toCheck) { + return argMultiMap.getValue(toCheck).isPresent(); + } +} diff --git a/src/main/java/fasttrack/logic/parser/edit/EditExpenseCommandParser.java b/src/main/java/fasttrack/logic/parser/edit/EditExpenseCommandParser.java new file mode 100644 index 00000000000..94daf37f691 --- /dev/null +++ b/src/main/java/fasttrack/logic/parser/edit/EditExpenseCommandParser.java @@ -0,0 +1,88 @@ +package fasttrack.logic.parser.edit; + +import static fasttrack.logic.parser.CliSyntax.PREFIX_CATEGORY; +import static fasttrack.logic.parser.CliSyntax.PREFIX_DATE; +import static fasttrack.logic.parser.CliSyntax.PREFIX_NAME; +import static fasttrack.logic.parser.CliSyntax.PREFIX_PRICE; + +import java.time.LocalDate; + +import fasttrack.commons.core.index.Index; +import fasttrack.logic.commands.edit.EditExpenseCommand; +import fasttrack.logic.parser.ArgumentMultimap; +import fasttrack.logic.parser.ArgumentTokenizer; +import fasttrack.logic.parser.Parser; +import fasttrack.logic.parser.ParserUtil; +import fasttrack.logic.parser.Prefix; +import fasttrack.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new EditExpenseCommand object + */ +public class EditExpenseCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the EditCategory + * @param args Arguments provided by user in String form. + * @return EditExpenseCommand that will carry out the user's arguments to edit the expense specified. + * @throws ParseException if the user input does not conform to required format. + */ + public EditExpenseCommand parse(String args) throws ParseException { + //First check if the given index is valid. + ArgumentMultimap argMultiMap = + ArgumentTokenizer.tokenize(args, PREFIX_CATEGORY, PREFIX_DATE, PREFIX_NAME, PREFIX_PRICE); + Index index = ParserUtil.parseIndex(argMultiMap.getPreamble()); + String newExpenseName; + Double newPrice; + LocalDate newDate; + String newCategory; + + //Get the new category name if applicable + if (isPrefixPresent(argMultiMap, PREFIX_CATEGORY)) { + newCategory = argMultiMap.getValue(PREFIX_CATEGORY).get(); + } else { + newCategory = null; + } + + if (isPrefixPresent(argMultiMap, PREFIX_NAME)) { + newExpenseName = argMultiMap.getValue(PREFIX_NAME).get(); + } else { + newExpenseName = null; + } + + if (isPrefixPresent(argMultiMap, PREFIX_PRICE)) { + String inputPriceInString = argMultiMap.getValue(PREFIX_PRICE).get(); + try { + newPrice = Double.parseDouble(inputPriceInString); + } catch (NumberFormatException nfe) { + throw new ParseException("Invalid price!", nfe); + } + } else { + newPrice = null; + } + + if (isPrefixPresent(argMultiMap, PREFIX_DATE)) { + String inputDateInString = argMultiMap.getValue(PREFIX_DATE).get(); + try { + newDate = ParserUtil.parseDate(inputDateInString); + } catch (ParseException pe) { + throw new ParseException("Invalid date format! Please use DD/MM/YY format!", pe); + } + } else { + newDate = null; + } + + return new EditExpenseCommand(index, newExpenseName, newPrice, newDate, newCategory); + } + + /** + * Returns true if the given prefix does not contain {@code Optional} values in the given {@code ArgumentMultimap} + * @param argMultiMap The argument multimap to check for. + * @param toCheck The prefix to check for. + * @return Boolean that indicates whether the given prefix is present or not. + */ + private static boolean isPrefixPresent(ArgumentMultimap argMultiMap, Prefix toCheck) { + return argMultiMap.getValue(toCheck).isPresent(); + } + +} diff --git a/src/main/java/fasttrack/logic/parser/edit/EditRecurringExpenseManagerCommandParser.java b/src/main/java/fasttrack/logic/parser/edit/EditRecurringExpenseManagerCommandParser.java new file mode 100644 index 00000000000..60ebb8eced7 --- /dev/null +++ b/src/main/java/fasttrack/logic/parser/edit/EditRecurringExpenseManagerCommandParser.java @@ -0,0 +1,99 @@ +package fasttrack.logic.parser.edit; + +import static fasttrack.logic.parser.CliSyntax.PREFIX_CATEGORY; +import static fasttrack.logic.parser.CliSyntax.PREFIX_END_DATE; +import static fasttrack.logic.parser.CliSyntax.PREFIX_NAME; +import static fasttrack.logic.parser.CliSyntax.PREFIX_PRICE; +import static fasttrack.logic.parser.CliSyntax.PREFIX_TIMESPAN; + +import java.time.LocalDate; + +import fasttrack.commons.core.index.Index; +import fasttrack.logic.commands.edit.EditRecurringExpenseManagerCommand; +import fasttrack.logic.parser.ArgumentMultimap; +import fasttrack.logic.parser.ArgumentTokenizer; +import fasttrack.logic.parser.Parser; +import fasttrack.logic.parser.ParserUtil; +import fasttrack.logic.parser.Prefix; +import fasttrack.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new EditRecurringExpenseManagerCommand object + */ +public class EditRecurringExpenseManagerCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the EditCategory + * @param args Arguments provided by user in String form. + * @return EditRecurringExpenseManagerCommand that will carry out the user's arguments to edit the recurring expense + * specified. + * @throws ParseException if the user input does not conform to required format. + */ + public EditRecurringExpenseManagerCommand parse(String args) throws ParseException { + //First check if the given index is valid. + ArgumentMultimap argMultiMap = + ArgumentTokenizer.tokenize(args, PREFIX_CATEGORY, PREFIX_END_DATE, PREFIX_NAME, PREFIX_PRICE, + PREFIX_TIMESPAN); + Index index = ParserUtil.parseIndex(argMultiMap.getPreamble()); + String newExpenseName; + Double newPrice; + LocalDate newEndDate; + String newCategory; + String newFrequency; + + if (isPrefixPresent(argMultiMap, PREFIX_CATEGORY)) { + newCategory = argMultiMap.getValue(PREFIX_CATEGORY).get(); + } else { + newCategory = null; + } + + if (isPrefixPresent(argMultiMap, PREFIX_NAME)) { + newExpenseName = argMultiMap.getValue(PREFIX_NAME).get(); + } else { + newExpenseName = null; + } + + if (isPrefixPresent(argMultiMap, PREFIX_PRICE)) { + String inputPriceInString = argMultiMap.getValue(PREFIX_PRICE).get(); + try { + newPrice = Double.parseDouble(inputPriceInString); + } catch (NumberFormatException nfe) { + throw new ParseException("Invalid price!", nfe); + } + } else { + newPrice = null; + } + + if (isPrefixPresent(argMultiMap, PREFIX_END_DATE)) { + String inputDateInString = argMultiMap.getValue(PREFIX_END_DATE).get(); + try { + newEndDate = ParserUtil.parseDate(inputDateInString); + } catch (ParseException pe) { + throw new ParseException("Invalid date format! Please use DD/MM/YY format!", pe); + } + } else { + newEndDate = null; + } + + if (isPrefixPresent(argMultiMap, PREFIX_TIMESPAN)) { + String inputFrequencyInString = argMultiMap.getValue(PREFIX_TIMESPAN).get(); + newFrequency = inputFrequencyInString.toUpperCase(); + } else { + newFrequency = null; + } + + return new EditRecurringExpenseManagerCommand(index, newExpenseName, newPrice, newCategory, newFrequency, + newEndDate); + + } + + /** + * Returns true if the given prefix does not contain {@code Optional} values in the given {@code ArgumentMultimap} + * @param argMultiMap The argument multimap to check for. + * @param toCheck The prefix to check for. + * @return Boolean that indicates whether the given prefix is present or not. + */ + private static boolean isPrefixPresent(ArgumentMultimap argMultiMap, Prefix toCheck) { + return argMultiMap.getValue(toCheck).isPresent(); + } +} diff --git a/src/main/java/seedu/address/logic/parser/exceptions/ParseException.java b/src/main/java/fasttrack/logic/parser/exceptions/ParseException.java similarity index 73% rename from src/main/java/seedu/address/logic/parser/exceptions/ParseException.java rename to src/main/java/fasttrack/logic/parser/exceptions/ParseException.java index 158a1a54c1c..ecc7cccf02c 100644 --- a/src/main/java/seedu/address/logic/parser/exceptions/ParseException.java +++ b/src/main/java/fasttrack/logic/parser/exceptions/ParseException.java @@ -1,6 +1,6 @@ -package seedu.address.logic.parser.exceptions; +package fasttrack.logic.parser.exceptions; -import seedu.address.commons.exceptions.IllegalValueException; +import fasttrack.commons.exceptions.IllegalValueException; /** * Represents a parse error encountered by a parser. diff --git a/src/main/java/fasttrack/model/AnalyticModel.java b/src/main/java/fasttrack/model/AnalyticModel.java new file mode 100644 index 00000000000..9513b290d73 --- /dev/null +++ b/src/main/java/fasttrack/model/AnalyticModel.java @@ -0,0 +1,102 @@ +package fasttrack.model; + +import fasttrack.model.util.AnalyticsType; +import javafx.beans.property.DoubleProperty; + +/** + * The AnalyticModelManager class represents the in-memory model of user analytics from the expense tracker. + * It provides various methods for calculating and retrieving statistics related to user expenses. + */ +public interface AnalyticModel { + /** + * Returns a DoubleProperty representing analytics data for a given AnalyticsType. + * @param type an AnalyticsType enum that specifies which type of analytics data to return + * @return a DoubleProperty representing the requested analytics data + * @throws IllegalArgumentException if the given analytics type is not supported + */ + DoubleProperty getAnalyticsData(AnalyticsType type) throws IllegalArgumentException; + + /** + * Calculates the total amount spent in the current month based + * on the filtered expenses list and updates the value of monthlySpent property + * @return DoubleProperty representing the monthly spent amount + */ + DoubleProperty getMonthlySpent(); + + /** + * Calculates remaining budget for the current month + * and updates the value of monthlyRemaining property + * @return DoubleProperty representing the remaining budget for the month + */ + DoubleProperty getMonthlyRemaining(); + + /** + * Calculates total amount spent during the current week + * based on the filtered expenses list and updates the value of weeklySpent property + * @return DoubleProperty representing the total amount spent during the current week + */ + DoubleProperty getWeeklySpent(); + + /** + * Calculates remaining budget for the current week + * and updates the value of weeklyRemaining property + * @return DoubleProperty representing the remaining budget for the week + */ + DoubleProperty getWeeklyRemaining(); + + /** + * Calculates percentage change in spending from the previous week + * to the current week and updates the value of weeklyChange property + * @return DoubleProperty representing percentage change + */ + DoubleProperty getWeeklyChange(); + + /** + * Calculates percentage change in spending from the previous month + * to the current month and updates the value of monthlyChange property + * @return DoubleProperty representing percentage change in spending + */ + DoubleProperty getMonthlyChange(); + + /** + * Calculates the total amount of money spent on all expenses + * @return a DoubleProperty representing the total amount spent + */ + DoubleProperty getTotalSpent(); + + /** + * Calculates and returns the percentage of the monthly budget that has been spent so far + * The percentage is capped at 100% if it exceeds 100 + * @return a DoubleProperty representing the percentage of budget spent + */ + DoubleProperty getBudgetPercentage(); + + /** + * Updates the currently referenced monthly budget in the GUI + * This method is called when the BudgetProperty in {@code ExpenseTracker} has changed. + * @param newBudget the new Budget object value which changed + */ + void updateMonthlyBudgetProperty(Budget newBudget); + + /** + * Updates the currently referenced weekly budget in the GUI + * This method is called when the BudgetProperty in {@code ExpenseTracker} has changed. + * @param newBudget the new Budget object value which changed + */ + void updateWeeklyBudgetProperty(Budget newBudget); + + /** + * Gets the value of the users current monthly budget + */ + double getBudget(); + + /** + * Gets the Property of the users current monthly budget + */ + DoubleProperty getMonthlyBudgetProperty(); + + /** + * A convenience method to re-calculate and update all statistics + */ + void updateAllStatistics(); +} diff --git a/src/main/java/fasttrack/model/AnalyticModelManager.java b/src/main/java/fasttrack/model/AnalyticModelManager.java new file mode 100644 index 00000000000..00dade97865 --- /dev/null +++ b/src/main/java/fasttrack/model/AnalyticModelManager.java @@ -0,0 +1,353 @@ +package fasttrack.model; + +import static java.util.Objects.requireNonNull; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.temporal.TemporalAdjusters; + +import fasttrack.model.category.Category; +import fasttrack.model.expense.Expense; +import fasttrack.model.util.AnalyticsType; +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.SimpleDoubleProperty; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; + +/** + * The AnalyticModelManager class represents the in-memory model of user analytics from the expense tracker. + * It provides various methods for calculating and retrieving statistics related to user expenses. + */ +public class AnalyticModelManager implements AnalyticModel { + private final ObservableList allExpenses; + private final ObservableList allCategories; + + private final DoubleProperty monthlySpent; + private final DoubleProperty monthlyRemaining; + private final DoubleProperty weeklySpent; + private final DoubleProperty weeklyRemaining; + private final DoubleProperty monthlyChange; + private final DoubleProperty weeklyChange; + private final DoubleProperty totalSpent; + private final DoubleProperty budgetPercentage; + private final DoubleProperty monthlyBudget; + private final DoubleProperty weeklyBudget; + private final LocalDate currentDate; + + /** + * Initializes an AnalyticModelManager with the given expense tracker data + * and a given date which serves as a point of reference from which analytics + * are generated + */ + public AnalyticModelManager(ReadOnlyExpenseTracker expenseTracker, LocalDate referenceDate) { + requireNonNull(expenseTracker); + requireNonNull(referenceDate); + allExpenses = expenseTracker.getExpenseList(); + allCategories = expenseTracker.getCategoryList(); + monthlyBudget = new SimpleDoubleProperty(0); + weeklyBudget = new SimpleDoubleProperty(0); + monthlySpent = new SimpleDoubleProperty(0); + monthlyRemaining = new SimpleDoubleProperty(0); + weeklySpent = new SimpleDoubleProperty(0); + weeklyRemaining = new SimpleDoubleProperty(0); + weeklyChange = new SimpleDoubleProperty(0); + monthlyChange = new SimpleDoubleProperty(0); + totalSpent = new SimpleDoubleProperty(0); + budgetPercentage = new SimpleDoubleProperty(0); + currentDate = referenceDate; + + updateAllStatistics(); + updateMonthlyBudgetProperty(expenseTracker.getBudgetForStats().get()); + updateWeeklyBudgetProperty(expenseTracker.getBudgetForStats().get()); + + allExpenses.addListener((ListChangeListener) expenseChange -> { + updateAllStatistics(); + }); + expenseTracker.getBudgetForStats().addListener((observable, oldValue, newValue) -> { + updateMonthlyBudgetProperty(newValue); + updateWeeklyBudgetProperty(newValue); + updateAllStatistics(); + }); + } + + /** + * Initializes an AnalyticModelManager with the given expense tracker data + * Date reference for analytics is taken to be the current date and time of construction + */ + public AnalyticModelManager(ReadOnlyExpenseTracker expenseTracker) { + this(expenseTracker, LocalDate.now()); + } + + + /** + * Calculates the total amount spent in the current month based + * on the filtered expenses list and updates the value of monthlySpent property + * @return DoubleProperty representing the monthly spent amount + */ + @Override + public DoubleProperty getMonthlySpent() { + double total = 0; + int currentYear = currentDate.getYear(); + int currentMonth = currentDate.getMonthValue(); + LocalDate startOfMonth = LocalDate.of(currentYear, currentMonth, 1); + LocalDate endOfMonth = startOfMonth.with(TemporalAdjusters.lastDayOfMonth()); + for (Expense expense: allExpenses) { + LocalDate expenseDate = expense.getDate(); + if (!expenseDate.isBefore(startOfMonth) && !expenseDate.isAfter(endOfMonth)) { + total += expense.getAmount(); + } + } + monthlySpent.set(total); + return monthlySpent; + } + + + /** + * Calculates remaining budget for the current month + * and updates the value of monthlyRemaining property + * @return DoubleProperty representing the remaining budget for the month + */ + @Override + public DoubleProperty getMonthlyRemaining() { + // Do not calculate monthly remaining if budget is 0 + if (monthlyBudget.get() == 0) { + monthlyRemaining.set(0); + return monthlyRemaining; + } + double remaining = monthlyBudget.get() - monthlySpent.get(); + monthlyRemaining.set(remaining); + return monthlyRemaining; + } + + /** + * Calculates total amount spent during the current week + * based on the filtered expenses list and updates the value of weeklySpent property + * @return DoubleProperty representing the total amount spent during the current week + */ + @Override + public DoubleProperty getWeeklySpent() { + double total = 0; + LocalDate weekStart = currentDate.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)); + LocalDate weekEnd = currentDate.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY)); + for (Expense expense: allExpenses) { + LocalDate expenseDate = expense.getDate(); + if (!expenseDate.isBefore(weekStart) && !expenseDate.isAfter(weekEnd)) { + total += expense.getAmount(); + } + } + weeklySpent.set(total); + return weeklySpent; + } + + /** + * Calculates remaining budget for the current week + * and updates the value of weeklyRemaining property + * @return DoubleProperty representing the remaining budget for the week + */ + @Override + public DoubleProperty getWeeklyRemaining() { + // Do not calculate weekly remaining if budget is 0 + if (monthlyBudget.get() == 0) { + weeklyRemaining.set(0); + return weeklyRemaining; + } + double remaining = weeklyBudget.get() - weeklySpent.get(); + weeklyRemaining.set(remaining); + return weeklyRemaining; + } + + /** + * Calculates percentage change in spending from the previous week + * to the current week and updates the value of weeklyChange property + * @return DoubleProperty representing percentage change + */ + @Override + public DoubleProperty getWeeklyChange() { + // Do not calculate weekly change if budget is 0 + if (monthlyBudget.get() == 0) { + weeklyChange.set(0); + return weeklyChange; + } + double previousWeekTotal = 0; + LocalDate previousWeekStart = currentDate.with(TemporalAdjusters.previous(DayOfWeek.MONDAY)).minusWeeks(1); + LocalDate previousWeekEnd = currentDate.with(TemporalAdjusters.previous(DayOfWeek.MONDAY)).minusDays(1); + for (Expense expense: allExpenses) { + LocalDate expenseDate = expense.getDate(); + if (!expenseDate.isBefore(previousWeekStart) && !expenseDate.isAfter(previousWeekEnd)) { + previousWeekTotal += expense.getAmount(); + } + } + double change; + if (previousWeekTotal == 0) { + change = 0; + } else { + change = (weeklySpent.get() - previousWeekTotal) / previousWeekTotal; + } + weeklyChange.set(change * 100); + return weeklyChange; + } + + /** + * Calculates percentage change in spending from the previous month + * to the current month and updates the value of monthlyChange property. + * @return DoubleProperty representing the percentage change + */ + @Override + public DoubleProperty getMonthlyChange() { + // Do not calculate monthly change if budget is 0 + if (monthlyBudget.get() == 0) { + monthlyChange.set(0); + return monthlyChange; + } + double previousMonthTotal = 0; + LocalDate previousMonthStart = currentDate.withDayOfMonth(1).minusMonths(1); + LocalDate previousMonthEnd = previousMonthStart.with(TemporalAdjusters.lastDayOfMonth()); + for (Expense expense: allExpenses) { + LocalDate expenseDate = expense.getDate(); + if (!expenseDate.isBefore(previousMonthStart) && !expenseDate.isAfter(previousMonthEnd)) { + previousMonthTotal += expense.getAmount(); + } + } + double change; + if (previousMonthTotal == 0) { + change = 0; + } else { + change = (monthlySpent.get() - previousMonthTotal) / previousMonthTotal; + } + monthlyChange.set(change * 100); + return monthlyChange; + } + + /** + * Calculates the total amount of money spent on all expenses + * @return a DoubleProperty representing the total amount spent + */ + @Override + public DoubleProperty getTotalSpent() { + double amount = allExpenses.stream().mapToDouble(Expense::getAmount).sum(); + totalSpent.set(amount); + return totalSpent; + } + + /** + * Calculates and returns the percentage of the monthly budget that has been spent so far + * The percentage is capped at 100% if it exceeds 100 + * @return a DoubleProperty representing the percentage of budget spent + */ + @Override + public DoubleProperty getBudgetPercentage() { + // Do not calculate budget percentage if budget is 0 + if (monthlyBudget.get() == 0) { + budgetPercentage.set(0); + return budgetPercentage; + } + double percentage = (monthlySpent.get() / monthlyBudget.get()) * 100; + if (percentage > 100) { + percentage = 100; + } + budgetPercentage.set(percentage); + return budgetPercentage; + } + + /** + * Updates the currently referenced monthly budget in the GUI + * This method is called when the BudgetProperty in {@code ExpenseTracker} has changed. + * @param newBudget the new Budget object value which changed + */ + @Override + public void updateMonthlyBudgetProperty(Budget newBudget) { + monthlyBudget.set(newBudget.getMonthlyBudget()); + } + + /** + * Updates the currently referenced weekly budget in the GUI + * This method is called when the BudgetProperty in {@code ExpenseTracker} has changed. + * @param newBudget the new Budget object value which changed + */ + @Override + public void updateWeeklyBudgetProperty(Budget newBudget) { + weeklyBudget.set(newBudget.getWeeklyBudget()); + } + + /** + * Returns the current value of the user's set monthly budget + */ + @Override + public double getBudget() { + return monthlyBudget.get(); + } + + /** + * Gets the Property of the users current monthly budget + */ + @Override + public DoubleProperty getMonthlyBudgetProperty() { + return monthlyBudget; + } + + /** + * Returns a DoubleProperty representing analytics data for a given AnalyticsType. + * @param type an AnalyticsType enum that specifies which type of analytics data to return + * @return a DoubleProperty representing the requested analytics data + * @throws IllegalArgumentException if the given analytics type is not supported + */ + @Override + public DoubleProperty getAnalyticsData(AnalyticsType type) throws IllegalArgumentException { + switch(type) { + case MONTHLY_SPENT: + return getMonthlySpent(); + case MONTHLY_REMAINING: + return getMonthlyRemaining(); + case WEEKLY_SPENT: + return getWeeklySpent(); + case WEEKLY_REMAINING: + return getWeeklyRemaining(); + case WEEKLY_CHANGE: + return getWeeklyChange(); + case MONTHLY_CHANGE: + return getMonthlyChange(); + case TOTAL_SPENT: + return getTotalSpent(); + case BUDGET_PERCENTAGE: + return getBudgetPercentage(); + default: + throw new IllegalArgumentException("Unsupported analytics type"); + } + } + + /** + * A convenience method to re-calculate and update all statistics + */ + @Override + public void updateAllStatistics() { + getTotalSpent(); + getMonthlySpent(); + getWeeklySpent(); + getMonthlyRemaining(); + getWeeklyRemaining(); + getWeeklyChange(); + getMonthlyChange(); + getBudgetPercentage(); + } + + @Override + public boolean equals(Object obj) { + // short circuit if same object + if (obj == this) { + return true; + } + + // instanceof handles nulls + if (!(obj instanceof AnalyticModelManager)) { + return false; + } + + // state check + AnalyticModelManager other = (AnalyticModelManager) obj; + return allExpenses.equals(other.allExpenses) + && allCategories.equals(other.allCategories) + && currentDate.equals(other.currentDate); + } +} + + diff --git a/src/main/java/fasttrack/model/Budget.java b/src/main/java/fasttrack/model/Budget.java new file mode 100644 index 00000000000..3babe8a757d --- /dev/null +++ b/src/main/java/fasttrack/model/Budget.java @@ -0,0 +1,47 @@ +package fasttrack.model; + + +/** + * Represents a budget which users of FastTrack can set. + */ +public class Budget { + private double monthBudget; + + public Budget(double budget) { + this.monthBudget = budget; + } + + /** + * Returns the monthlyBudget. + * @return double + */ + public double getMonthlyBudget() { + return this.monthBudget; + } + + /** + * Returns the weekly budget. + * @return double + */ + public double getWeeklyBudget() { + return this.monthBudget / 4; + } + + + @Override + public String toString() { + return Double.toString(this.monthBudget); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Budget // instanceof handles nulls + && monthBudget == ((Budget) other).monthBudget); + } + + @Override + public int hashCode() { + return Double.hashCode(monthBudget); + } +} diff --git a/src/main/java/fasttrack/model/ExpenseTracker.java b/src/main/java/fasttrack/model/ExpenseTracker.java new file mode 100644 index 00000000000..4eaa1393a8b --- /dev/null +++ b/src/main/java/fasttrack/model/ExpenseTracker.java @@ -0,0 +1,271 @@ +package fasttrack.model; + +import static java.util.Objects.requireNonNull; + +import java.util.List; +import java.util.Objects; + +import fasttrack.model.category.Category; +import fasttrack.model.category.UniqueCategoryList; +import fasttrack.model.expense.Expense; +import fasttrack.model.expense.ExpenseList; +import fasttrack.model.expense.RecurringExpenseList; +import fasttrack.model.expense.RecurringExpenseManager; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.ObservableList; + + +/** + * Wraps all data at the expense tracker level + * Duplicate categories are not allowed (by .isSameCategory comparison) + */ +public class ExpenseTracker implements ReadOnlyExpenseTracker { + + private final UniqueCategoryList categories; + private final ExpenseList expenses; + private final RecurringExpenseList recurringGenerators; + private final ObjectProperty simpleBudget; + + /* + * The 'unusual' code block below is a non-static initialization block, + * sometimes used to avoid duplication + * between constructors. See + * https://docs.oracle.com/javase/tutorial/java/javaOO/initial.html + * + * Note that non-static init blocks are not recommended to use. There are other + * ways to avoid duplication + * among constructors. + */ + { + categories = new UniqueCategoryList(); + expenses = new ExpenseList(); + simpleBudget = new SimpleObjectProperty<>(new Budget(0)); + recurringGenerators = new RecurringExpenseList(); + } + + public ExpenseTracker() { + } + + /** + * Creates an ExpenseTracker using the data in the {@code toBeCopied} + */ + public ExpenseTracker(ReadOnlyExpenseTracker toBeCopied) { + this(); + resetData(toBeCopied); + } + + //// list overwrite operations + + /** + * Replaces the contents of the category list with {@code categories}. + * {@code categories} must not contain duplicate categories. + */ + public void setCategories(List categories) { + this.categories.setCategoryList(categories); + } + + /** + * Replaces the contents of the expense list with {@code expenses}. + */ + public void setExpenses(List expenses) { + this.expenses.setExpenseList(expenses); + } + + public void setBudget(Budget budget) { + this.simpleBudget.set(budget); + } + + public void setRecurringExpenseGenerators(List recurringExpenseGenerators) { + this.recurringGenerators.setRecurringExpenseList(recurringExpenseGenerators); + } + + /** + * Resets the existing data of this {@code ExpenseTracker} with {@code newData}. + */ + public void resetData(ReadOnlyExpenseTracker newData) { + requireNonNull(newData); + setExpenses(newData.getExpenseList()); + setCategories(newData.getCategoryList()); + setBudget(newData.getBudget()); + setRecurringExpenseGenerators(newData.getRecurringExpenseGenerators()); + generateRetroactiveExpenses(); + expenses.sortList(); + cleanupExpiredRecurringExpenses(); + } + + /** + * Adds expenses retroactively for recurring expenses which have starting dates that need to be added. + */ + public void generateRetroactiveExpenses() { + for (RecurringExpenseManager generators : recurringGenerators.getRecurringExpenseList()) { + for (Expense expense : generators.getExpenses()) { + addExpense(expense); + } + } + expenses.sortList(); + } + + /** + * Removes RecurringExpenseManager objects that are expired. + */ + public void cleanupExpiredRecurringExpenses() { + recurringGenerators.cleanupExpiredGenerators(); + } + + //// category-level operations + /** + * Returns true if the given category exists in the list. + * @param category The category to check for existence in the list. + * @return true if the category exists in the list and false otherwise. + */ + public boolean hasCategory(Category category) { + requireNonNull(category); + return categories.contains(category); + } + + /** + * Adds a category to the expense tracker. + * The category must not already exist in the expense tracker. + */ + public void addCategory(Category toAdd) { + categories.add(toAdd); + } + + /** + * Deletes the given category {@code key} in the UniqueCategoryList. + * Replaces all expenses with {@code key} with the MiscellaneousCategory object. + * @param key + */ + public void removeCategory(Category key) { + categories.remove(key); + expenses.replaceDeletedCategory(key); + recurringGenerators.replaceDeletedCategory(key); + } + + @Override + public int hashCode() { + return Objects.hash(categories, expenses); + } + + public Category getCategoryInstance(Category category) { + for (Category c : categories.asUnmodifiableList()) { + if (category.equals(c)) { + return c; + } + } + return null; + } + + //// util methods + + @Override + public String toString() { + return expenses.asUnmodifiableList().size() + " expenses"; + } + + @Override + public ObservableList getCategoryList() { + return categories.asUnmodifiableList(); + } + + //// expense-level operations + + @Override + public ObservableList getExpenseList() { + return expenses.asUnmodifiableList(); + } + + @Override + public ObservableList getRecurringExpenseGenerators() { + return recurringGenerators.asUnmodifiableList(); + } + @Override + public Budget getBudget() { + return this.simpleBudget.get(); + } + + @Override + public ObjectProperty getBudgetForStats() { + return this.simpleBudget; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof ExpenseTracker // instanceof handles nulls + && expenses.equals(((ExpenseTracker) other).expenses) + && categories.equals(((ExpenseTracker) other).categories)); + } + + /** + * Adds an expense to the expense tracker. + * @param expense to be added. + */ + public void addExpense(Expense expense) { + expenses.add(expense); + expenses.sortList(); + } + + /** + * Deletes an expense from the expense tracker. + * @param expense to be deleted. + */ + public void removeExpense(Expense expense) { + expenses.remove(expense); + expenses.sortList(); + } + + /** + * Sets an expense at the specified index. + * @param index index to be used. + * @param expense expense to be used to overwrite the previous expense. + */ + public void setExpense(int index, Expense expense) { + expenses.set(index, expense); + expenses.sortList(); + } + + public void setExpense(Expense target, Expense editedExpense) { + expenses.setExpense(target, editedExpense); + expenses.sortList(); + } + + /** + * Delete all Expense. + */ + public void clearExpense() { + expenses.clear(); + } + + public void clearCategory() { + categories.clear(); + } + + public void clearRecurringExpense() { + recurringGenerators.clear(); + } + + /** + * Returns true if the given expense exists in the list. + * @param expense The expense to check for existence in the list. + * @return true if the expense exists in the list and false otherwise. + */ + public boolean hasExpense(Expense expense) { + requireNonNull(expense); + return expenses.contains(expense); + } + + public boolean hasRecurringExpense(RecurringExpenseManager recurringExpenseManager) { + return recurringGenerators.contains(recurringExpenseManager); + } + + public void addRecurringGenerator(RecurringExpenseManager generator) { + recurringGenerators.addRecurringExpense(generator); + } + + + public void removeRecurringExpense(RecurringExpenseManager recurringExpenseManager) { + recurringGenerators.removeRecurringExpense(recurringExpenseManager); + } +} diff --git a/src/main/java/fasttrack/model/Model.java b/src/main/java/fasttrack/model/Model.java new file mode 100644 index 00000000000..23438090518 --- /dev/null +++ b/src/main/java/fasttrack/model/Model.java @@ -0,0 +1,204 @@ +package fasttrack.model; + +import java.nio.file.Path; +import java.util.function.Predicate; + +import fasttrack.commons.core.GuiSettings; +import fasttrack.logic.parser.ParserUtil; +import fasttrack.model.category.Category; +import fasttrack.model.expense.Expense; +import fasttrack.model.expense.RecurringExpenseManager; +import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.ObservableList; + +/** + * The API of the DataModel component. + */ +public interface Model { + + /** {@code Predicate} that always evaluate to true */ + Predicate PREDICATE_SHOW_ALL_EXPENSES = unused -> true; + Predicate PREDICATE_SHOW_ALL_CATEGORY = unused -> true; + + /** + * Replaces user prefs data with the data in {@code userPrefs}. + */ + void setUserPrefs(ReadOnlyUserPrefs userPrefs); + + /** + * Returns the user prefs. + */ + ReadOnlyUserPrefs getUserPrefs(); + + /** + * Returns the user prefs' GUI settings. + */ + GuiSettings getGuiSettings(); + + /** + * Sets the user prefs' GUI settings. + */ + void setGuiSettings(GuiSettings guiSettings); + + /** + * Returns the user prefs' expense tracker file path. + */ + Path getExpenseTrackerFilePath(); + + /** + * Sets the user prefs' expense tracker file path. + */ + void setExpenseTrackerFilePath(Path expenseTrackerFilePath); + + /** + * Replaces expenseTracker data with the data in {@code expenseTracker}. + */ + void setExpenseTracker(ReadOnlyExpenseTracker expenseTracker); + + /** Returns the ExpenseTracker */ + ReadOnlyExpenseTracker getExpenseTracker(); + + SimpleObjectProperty getAppliedTimeSpanFilter(); + + SimpleObjectProperty getAppliedCategoryFilter(); + + void updateTimeSpanFilter(ParserUtil.Timespan timeSpan); + + void updateCategoryFilter(Category category); + + // Expense accessor functions + + /** + * Adds the given expense to the expense tracker + * @param expense the new expense to add + */ + void addExpense(Expense expense); + + /** + * Deletes the given expense. + * The expense must exist in the ExpenseTracker. + * @param expense the expense to delete + */ + void deleteExpense(Expense expense); + + /** + * Delete all expense. + */ + void clearExpense(); + + + /** + * Replaces the Expense in the expense list at the given index. + * @param index + * @param expense + */ + void setExpense(int index, Expense expense); + + /** + * Replaces the given expense {@code target} with {@code editedExpense}. + * {@code target} must exist in the expense list + */ + void setExpense(Expense target, Expense editedExpense); + + + /** + * Indicates if an expense exists in the expense list + * @param expense the expense to check for + */ + boolean hasExpense(Expense expense); + + /** + * Updates the filter of the filtered expense list to filter by the given + * {@code predicate}. + * @throws NullPointerException if {@code predicate} is null. + */ + void updateFilteredExpensesList(Predicate predicate); + + /** Returns an unmodifiable view of the filtered expense list */ + ObservableList getFilteredExpenseList(); + + // Category accessor functions + + /** + * Deletes the given expense. + * @param target the category to delete + */ + void deleteCategory(Category target); + + /** + * Delete all Category. + */ + void clearCategory(); + + /** + * Adds the given category to the category list. + * @param toAdd the category to add + */ + void addCategory(Category toAdd); + + + /** + * Indicates if a category exists in the category list + * @param category the category to check for + */ + boolean hasCategory(Category category); + + /** Returns an unmodifiable view of the category list */ + ObservableList getFilteredCategoryList(); + + /** + * Updates the filter of the filtered category list to filter by the given + * {@code predicate}. + * @throws NullPointerException if {@code predicate} is null. + */ + void updateFilteredCategoryList(Predicate predicate); + + /** + * Returns a reference to the instance of category + * matching the category name in the category list + * @param category the category to check for + * @return the category instance if it exists, and null if it does not + */ + Category getCategoryInstance(Category category); + + /** + * Sets budget for FastTrack. + * @param budget + */ + void setBudget(Budget budget); + + /** + * Indicates if a RecurringExpense exists in the RecurringExpenseList + * @param recurringExpense the RecurringExpense to check for + */ + boolean hasRecurringExpense(RecurringExpenseManager recurringExpense); + + /** + * Adds a RecurringExpense to the RecurringExpense list. + * @param recurringExpenseManager the Recurring expense to add. + */ + void addRecurringGenerator(RecurringExpenseManager recurringExpenseManager); + + /** Returns an unmodifiable view of the recurring expense list */ + ObservableList getRecurringExpenseGenerators(); + + /** + * Updates the filter of the filtered recurring expense manager list to filter by the given + * {@code predicate}. + * @throws NullPointerException if {@code predicate} is null. + */ + void updateFilteredRecurringGenerators(Predicate predicate); + + /** + * Delete all recurring expense generators. + */ + void clearRecurringExpenseGenerator(); + + /** + * Deletes the target {@code RecurringExpense} from the recurring expense list. + * @param recurringExpenseManager the recurring expense to be deleted. + */ + void deleteRecurringExpense(RecurringExpenseManager recurringExpenseManager); + + void addRetroactiveExpenses(); +} diff --git a/src/main/java/fasttrack/model/ModelManager.java b/src/main/java/fasttrack/model/ModelManager.java new file mode 100644 index 00000000000..e9925787646 --- /dev/null +++ b/src/main/java/fasttrack/model/ModelManager.java @@ -0,0 +1,303 @@ +package fasttrack.model; + +import static fasttrack.commons.util.CollectionUtil.requireAllNonNull; +import static java.util.Objects.requireNonNull; + +import java.nio.file.Path; +import java.util.function.Predicate; +import java.util.logging.Logger; + +import fasttrack.commons.core.GuiSettings; +import fasttrack.commons.core.LogsCenter; +import fasttrack.logic.parser.ParserUtil; +import fasttrack.model.category.Category; +import fasttrack.model.expense.Expense; +import fasttrack.model.expense.RecurringExpenseManager; +import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.ObservableList; +import javafx.collections.transformation.FilteredList; + +/** + * Represents the in-memory model of the expense tracker data. + */ +public class ModelManager implements Model { + private static final Logger logger = LogsCenter.getLogger(ModelManager.class); + + private final ExpenseTracker expenseTracker; + private final UserPrefs userPrefs; + private final FilteredList filteredExpenses; + private final FilteredList filteredCategories; + private final FilteredList filteredRecurringExpense; + + private SimpleObjectProperty appliedCategoryFilter = + new SimpleObjectProperty<>(null); + private SimpleObjectProperty appliedTimeSpanFilter = + new SimpleObjectProperty<>(ParserUtil.Timespan.ALL); + + /** + * Initializes a ModelManager with the given expenseTracker and userPrefs. + */ + public ModelManager(ReadOnlyExpenseTracker expenseTracker, ReadOnlyUserPrefs userPrefs) { + requireAllNonNull(expenseTracker, userPrefs); + logger.fine("Initializing with expense tracker: " + expenseTracker + " and user prefs " + userPrefs); + this.expenseTracker = new ExpenseTracker(expenseTracker); + this.userPrefs = new UserPrefs(userPrefs); + filteredExpenses = new FilteredList<>(this.expenseTracker.getExpenseList()); + filteredCategories = new FilteredList<>(this.expenseTracker.getCategoryList()); + filteredRecurringExpense = new FilteredList<>(this.expenseTracker.getRecurringExpenseGenerators()); + } + + public ModelManager() { + this(new ExpenseTracker(), new UserPrefs()); + } + + + // =========== UserPrefs + // ================================================================================== + + @Override + public void setUserPrefs(ReadOnlyUserPrefs userPrefs) { + requireNonNull(userPrefs); + this.userPrefs.resetData(userPrefs); + } + + @Override + public ReadOnlyUserPrefs getUserPrefs() { + return userPrefs; + } + + @Override + public GuiSettings getGuiSettings() { + return userPrefs.getGuiSettings(); + } + + @Override + public void setGuiSettings(GuiSettings guiSettings) { + requireNonNull(guiSettings); + userPrefs.setGuiSettings(guiSettings); + } + + @Override + public Path getExpenseTrackerFilePath() { + return userPrefs.getExpenseTrackerFilePath(); + } + + @Override + public void setExpenseTrackerFilePath(Path expenseTrackerFilePath) { + requireNonNull(expenseTrackerFilePath); + userPrefs.setExpenseTrackerFilePath(expenseTrackerFilePath); + } + + // =========== ExpenseTracker + // ================================================================================ + + @Override + public void setExpenseTracker(ReadOnlyExpenseTracker expenseTracker) { + this.expenseTracker.resetData(expenseTracker); + } + + @Override + public ReadOnlyExpenseTracker getExpenseTracker() { + return expenseTracker; + } + + // =========== Expenses List Accessors + // ============================================================= + @Override + public boolean equals(Object obj) { + // short circuit if same object + if (obj == this) { + return true; + } + + // instanceof handles nulls + if (!(obj instanceof ModelManager)) { + return false; + } + + // state check + ModelManager other = (ModelManager) obj; + return expenseTracker.equals(other.expenseTracker) + && userPrefs.equals(other.userPrefs) + && filteredExpenses.equals(other.filteredExpenses) + && filteredCategories.equals(other.filteredCategories) + && filteredRecurringExpense.equals(other.filteredRecurringExpense); + } + + @Override + public SimpleObjectProperty getAppliedTimeSpanFilter() { + return appliedTimeSpanFilter; + } + + @Override + public SimpleObjectProperty getAppliedCategoryFilter() { + return appliedCategoryFilter; + } + + @Override + public void updateTimeSpanFilter(ParserUtil.Timespan timeSpan) { + appliedTimeSpanFilter.set(timeSpan); + } + + @Override + public void updateCategoryFilter(Category category) { + appliedCategoryFilter.set(category); + } + + // =========== Category List Accessors + // ============================================================= + + @Override + public ObservableList getFilteredCategoryList() { + return filteredCategories; + } + + @Override + public void updateFilteredCategoryList(Predicate predicate) { + requireNonNull(predicate); + filteredCategories.setPredicate(predicate); + } + + /** + * Indicates if a category exists in the category list + * @param category the category to check for + */ + @Override + public boolean hasCategory(Category category) { + requireNonNull(category); + return expenseTracker.hasCategory(category); + } + + @Override + public void addCategory(Category toAdd) { + expenseTracker.addCategory(toAdd); + } + + @Override + public void deleteCategory(Category target) { + expenseTracker.removeCategory(target); + updateFilteredCategoryList(PREDICATE_SHOW_ALL_CATEGORY); + updateFilteredExpensesList(PREDICATE_SHOW_ALL_EXPENSES); + } + + @Override + public void clearCategory() { + expenseTracker.clearCategory(); + } + + @Override + public Category getCategoryInstance(Category category) { + if (hasCategory(category)) { + return expenseTracker.getCategoryInstance(category); + } + return null; + } + + + // =========== Filtered Expense List Accessors + // ============================================================= + + /** + * Returns an unmodifiable view of the list of {@code Expense} backed by the + * internal list of + * {@code versionedAddressBook} + */ + @Override + public ObservableList getFilteredExpenseList() { + return filteredExpenses; + } + + /** + * Replaces the given expense {@code target} with {@code editedExpense}. + * {@code target} must exist in the expense list + */ + @Override + public void setExpense(Expense target, Expense editedExpense) { + expenseTracker.setExpense(target, editedExpense); + } + + @Override + public void setExpense(int index, Expense newExpense) { + expenseTracker.setExpense(index, newExpense); + } + + @Override + public void updateFilteredExpensesList(Predicate predicate) { + requireNonNull(predicate); + filteredExpenses.setPredicate(predicate); + } + + @Override + public void addExpense(Expense expense) { + expenseTracker.addExpense(expense); + updateFilteredExpensesList(PREDICATE_SHOW_ALL_EXPENSES); + } + + @Override + public void deleteExpense(Expense expense) { + expenseTracker.removeExpense(expense); + updateFilteredExpensesList(PREDICATE_SHOW_ALL_EXPENSES); + } + + @Override + public void clearExpense() { + expenseTracker.clearExpense(); + } + + /** + * Indicates if an expense exists in the expense list + * @param expense the expense to check for + */ + @Override + public boolean hasExpense(Expense expense) { + requireNonNull(expense); + return expenseTracker.hasExpense(expense); + } + + @Override + public void setBudget(Budget budget) { + expenseTracker.setBudget(budget); + } + + // =========== Recurring Expense Manager List Accessors + // ============================================================= + + @Override + public boolean hasRecurringExpense(RecurringExpenseManager recurringExpenseManager) { + return expenseTracker.hasRecurringExpense(recurringExpenseManager); + } + + @Override + public void addRecurringGenerator(RecurringExpenseManager recurringExpenseManager) { + expenseTracker.addRecurringGenerator(recurringExpenseManager); + } + + @Override + public ObservableList getRecurringExpenseGenerators() { + return expenseTracker.getRecurringExpenseGenerators(); + } + + @Override + public void updateFilteredRecurringGenerators(Predicate predicate) { + requireNonNull(predicate); + filteredRecurringExpense.setPredicate(predicate); + } + + /** + * Delete all recurring expense generators. + */ + @Override + public void clearRecurringExpenseGenerator() { + expenseTracker.clearRecurringExpense(); + } + + @Override + public void deleteRecurringExpense(RecurringExpenseManager recurringExpenseManager) { + expenseTracker.removeRecurringExpense(recurringExpenseManager); + } + + @Override + public void addRetroactiveExpenses() { + expenseTracker.generateRetroactiveExpenses(); + } +} diff --git a/src/main/java/fasttrack/model/ReadOnlyExpenseTracker.java b/src/main/java/fasttrack/model/ReadOnlyExpenseTracker.java new file mode 100644 index 00000000000..02bd307892c --- /dev/null +++ b/src/main/java/fasttrack/model/ReadOnlyExpenseTracker.java @@ -0,0 +1,31 @@ +package fasttrack.model; + +import fasttrack.model.category.Category; +import fasttrack.model.expense.Expense; +import fasttrack.model.expense.RecurringExpenseManager; +import javafx.beans.property.ObjectProperty; +import javafx.collections.ObservableList; + +/** + * Unmodifiable view of an expense tracker + */ +public interface ReadOnlyExpenseTracker { + + /** + * Returns an unmodifiable view of the category list. + * This list will not contain any duplicate categories. + */ + ObservableList getCategoryList(); + + /** + * Returns an unmodifiable view of the expense list. + */ + ObservableList getExpenseList(); + + Budget getBudget(); + + ObjectProperty getBudgetForStats(); + + ObservableList getRecurringExpenseGenerators(); + +} diff --git a/src/main/java/seedu/address/model/ReadOnlyUserPrefs.java b/src/main/java/fasttrack/model/ReadOnlyUserPrefs.java similarity index 57% rename from src/main/java/seedu/address/model/ReadOnlyUserPrefs.java rename to src/main/java/fasttrack/model/ReadOnlyUserPrefs.java index befd58a4c73..4425b97cd3e 100644 --- a/src/main/java/seedu/address/model/ReadOnlyUserPrefs.java +++ b/src/main/java/fasttrack/model/ReadOnlyUserPrefs.java @@ -1,8 +1,8 @@ -package seedu.address.model; +package fasttrack.model; import java.nio.file.Path; -import seedu.address.commons.core.GuiSettings; +import fasttrack.commons.core.GuiSettings; /** * Unmodifiable view of user prefs. @@ -11,6 +11,6 @@ public interface ReadOnlyUserPrefs { GuiSettings getGuiSettings(); - Path getAddressBookFilePath(); + Path getExpenseTrackerFilePath(); } diff --git a/src/main/java/seedu/address/model/UserPrefs.java b/src/main/java/fasttrack/model/UserPrefs.java similarity index 68% rename from src/main/java/seedu/address/model/UserPrefs.java rename to src/main/java/fasttrack/model/UserPrefs.java index 25a5fd6eab9..45a18c88239 100644 --- a/src/main/java/seedu/address/model/UserPrefs.java +++ b/src/main/java/fasttrack/model/UserPrefs.java @@ -1,4 +1,4 @@ -package seedu.address.model; +package fasttrack.model; import static java.util.Objects.requireNonNull; @@ -6,7 +6,7 @@ import java.nio.file.Paths; import java.util.Objects; -import seedu.address.commons.core.GuiSettings; +import fasttrack.commons.core.GuiSettings; /** * Represents User's preferences. @@ -14,7 +14,7 @@ public class UserPrefs implements ReadOnlyUserPrefs { private GuiSettings guiSettings = new GuiSettings(); - private Path addressBookFilePath = Paths.get("data" , "addressbook.json"); + private Path expenseTrackerFilePath = Paths.get("data" , "fastTrack.json"); /** * Creates a {@code UserPrefs} with default values. @@ -35,9 +35,10 @@ public UserPrefs(ReadOnlyUserPrefs userPrefs) { public void resetData(ReadOnlyUserPrefs newUserPrefs) { requireNonNull(newUserPrefs); setGuiSettings(newUserPrefs.getGuiSettings()); - setAddressBookFilePath(newUserPrefs.getAddressBookFilePath()); + setExpenseTrackerFilePath(newUserPrefs.getExpenseTrackerFilePath()); } + @Override public GuiSettings getGuiSettings() { return guiSettings; } @@ -47,13 +48,14 @@ public void setGuiSettings(GuiSettings guiSettings) { this.guiSettings = guiSettings; } - public Path getAddressBookFilePath() { - return addressBookFilePath; + @Override + public Path getExpenseTrackerFilePath() { + return expenseTrackerFilePath; } - public void setAddressBookFilePath(Path addressBookFilePath) { - requireNonNull(addressBookFilePath); - this.addressBookFilePath = addressBookFilePath; + public void setExpenseTrackerFilePath(Path expenseTrackerFilePath) { + requireNonNull(expenseTrackerFilePath); + this.expenseTrackerFilePath = expenseTrackerFilePath; } @Override @@ -68,19 +70,19 @@ public boolean equals(Object other) { UserPrefs o = (UserPrefs) other; return guiSettings.equals(o.guiSettings) - && addressBookFilePath.equals(o.addressBookFilePath); + && expenseTrackerFilePath.equals(o.expenseTrackerFilePath); } @Override public int hashCode() { - return Objects.hash(guiSettings, addressBookFilePath); + return Objects.hash(guiSettings, expenseTrackerFilePath); } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("Gui Settings : " + guiSettings); - sb.append("\nLocal data file location : " + addressBookFilePath); + sb.append("\nLocal data file location : " + expenseTrackerFilePath); return sb.toString(); } diff --git a/src/main/java/fasttrack/model/category/Category.java b/src/main/java/fasttrack/model/category/Category.java new file mode 100644 index 00000000000..447d878829e --- /dev/null +++ b/src/main/java/fasttrack/model/category/Category.java @@ -0,0 +1,64 @@ +package fasttrack.model.category; + +import java.util.Objects; + +/** + * Category class to represent categories that expenses are grouped under. + */ +public abstract class Category { + + public static final String MESSAGE_CONSTRAINTS = "Category names should be alphanumeric"; + public static final String VALIDATION_REGEX = "[\\p{Alnum}][\\p{Alnum} ]*"; + + protected String categoryName; + protected String summary; + + /** + * Constructor for Category class. + * @param categoryName Name of the category + * @param summary Short description of the category + */ + public Category(String categoryName, String summary) { + this.categoryName = categoryName; + this.summary = summary; + } + + /** + * Returns true if a given string is a valid category name. + */ + public static boolean isValidCategoryName(String categoryName) { + return categoryName.matches(VALIDATION_REGEX) && !categoryName.isBlank(); + } + + public String getCategoryName() { + return this.categoryName; + }; + + public String getSummary() { + return this.summary; + }; + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof Category)) { + return false; + } + + Category otherCategory = (Category) other; + return otherCategory.getCategoryName().strip().toLowerCase().equals(getCategoryName().strip().toLowerCase()); + } + + @Override + public int hashCode() { + return Objects.hash(categoryName, summary); + } + + @Override + public String toString() { + return this.categoryName; + } +} diff --git a/src/main/java/fasttrack/model/category/MiscellaneousCategory.java b/src/main/java/fasttrack/model/category/MiscellaneousCategory.java new file mode 100644 index 00000000000..9654c22cf2c --- /dev/null +++ b/src/main/java/fasttrack/model/category/MiscellaneousCategory.java @@ -0,0 +1,31 @@ +package fasttrack.model.category; + +/** +* Category class to represent categories that expenses are not grouped into a +* specific category. +*/ +public class MiscellaneousCategory extends Category { + /** + * Constructor for MiscellaneousCategory class. + */ + public MiscellaneousCategory() { + super("Misc", "Placeholder Description"); + } + + @Override + public String toString() { + return "Miscellaneous"; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof MiscellaneousCategory // instanceof handles nulls + && this.categoryName.equals(((MiscellaneousCategory) other).categoryName)); // state check + } + + @Override + public int hashCode() { + return categoryName.hashCode(); + } +} diff --git a/src/main/java/fasttrack/model/category/UniqueCategoryList.java b/src/main/java/fasttrack/model/category/UniqueCategoryList.java new file mode 100644 index 00000000000..a5ff20913cd --- /dev/null +++ b/src/main/java/fasttrack/model/category/UniqueCategoryList.java @@ -0,0 +1,135 @@ +package fasttrack.model.category; + +import static fasttrack.commons.util.CollectionUtil.requireAllNonNull; +import static java.util.Objects.requireNonNull; + +import java.util.Iterator; +import java.util.List; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +/** + * A list of categories that enforces uniqueness between its elements and does not allow nulls. + */ +public class UniqueCategoryList implements Iterable { + + private final ObservableList internalListOfCategories = FXCollections.observableArrayList(); + private final ObservableList internalUnmodifiableList = FXCollections + .unmodifiableObservableList(internalListOfCategories); + + /** + * Returns true if the list contains an equivalent category as the given argument. + * @param category Category to check for + */ + public boolean contains(Category category) { + requireNonNull(category); + return internalListOfCategories.stream().anyMatch(category::equals); + } + + /** + * Adds a category to the list. + * @param newCategory Category to add + */ + public void add(Category newCategory) { + requireNonNull(newCategory); + + if (contains(newCategory)) { + //Throw an exception here later + } + internalListOfCategories.add(newCategory); + } + + /** + * Replaces the category {@code target} in the list with {@code editedCategory}. + * {@code target} must exist in the list. + * @param category Category to remove + */ + public void remove(Category category) { + requireNonNull(category); + + if (!internalListOfCategories.remove(category)) { + //Throw an exception here later + } + } + + /** + * Delete all Category. + */ + public void clear() { + internalListOfCategories.clear(); + internalUnmodifiableList.clear(); + } + + /** + * Replaces the category {@code target} in the list with {@code editedCategory}. + * {@code target} must exist in the list. + * The category identity of {@code editedCategory} must not be + * the same as another existing category in the list. + * @param replacementList List of categories to replace the current list + */ + public void setCategoryList(UniqueCategoryList replacementList) { + requireNonNull(replacementList); + internalListOfCategories.setAll(replacementList.internalListOfCategories); + } + + /** + * Replaces the category {@code target} in the list with {@code editedCategory}. + * {@code target} must exist in the list. + * The category identity of {@code editedCategory} must not be + * the same as another existing category in the list. + * @param listOfCategories List of categories to replace the current list + */ + public void setCategoryList(List listOfCategories) { + requireAllNonNull(listOfCategories); + if (!categoriesAreUnique(listOfCategories)) { + //Throw an exception here + } + internalListOfCategories.setAll(listOfCategories); + } + + /** + * Returns the backing list as an unmodifiable {@code ObservableList}. + * This list will not contain any null categories. + * @param listOfCategories List of categories to check for uniqueness + */ + public boolean categoriesAreUnique(List listOfCategories) { + for (int i = 0; i < listOfCategories.size(); i++) { + for (int j = i + 1; j < listOfCategories.size(); j++) { + if (listOfCategories.get(i).equals(listOfCategories.get(j))) { + return false; + } + } + } + return true; + } + /** + * Returns the backing list as an unmodifiable {@code ObservableList}. + * This list will not contain any null categories. + */ + public ObservableList asUnmodifiableList() { + return this.internalUnmodifiableList; + } + + @Override + public Iterator iterator() { + return this.internalListOfCategories.iterator(); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof UniqueCategoryList)) { + return false; + } + UniqueCategoryList otherInUniqueList = (UniqueCategoryList) other; + return this.internalListOfCategories.equals(otherInUniqueList.internalListOfCategories); + } + + @Override + public int hashCode() { + return internalListOfCategories.hashCode(); + } +} diff --git a/src/main/java/fasttrack/model/category/UserDefinedCategory.java b/src/main/java/fasttrack/model/category/UserDefinedCategory.java new file mode 100644 index 00000000000..309ddc7ddbf --- /dev/null +++ b/src/main/java/fasttrack/model/category/UserDefinedCategory.java @@ -0,0 +1,24 @@ +package fasttrack.model.category; + +/** + * User-defined category class which allows users to customize their own + * categories to use. + */ +public class UserDefinedCategory extends Category { + /** + * Constructor for UserDefinedCategory class. + * @param categoryName Name of the category + * @param summary Short description of the category + */ + public UserDefinedCategory(String categoryName, String summary) { + super(categoryName, summary); + } + + public void setCategoryName(String categoryName) { + this.categoryName = categoryName.replaceAll("\\s+", " ");; + } + + public void setDescription(String summary) { + this.summary = summary.replaceAll("\\s+", " "); + } +} diff --git a/src/main/java/fasttrack/model/expense/Expense.java b/src/main/java/fasttrack/model/expense/Expense.java new file mode 100644 index 00000000000..c3cc2417212 --- /dev/null +++ b/src/main/java/fasttrack/model/expense/Expense.java @@ -0,0 +1,150 @@ +package fasttrack.model.expense; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.Objects; + +import fasttrack.model.category.Category; + +/** + * Represents an Expense in the expense tracker. + * Guarantees: details are present and not null, field values are validated, + * immutable. + * @author shirsho-12 + * @version 1.0 + */ +public class Expense { + + public static final String MESSAGE_CONSTRAINTS = "Expense names should be alphanumeric"; + public static final String VALIDATION_REGEX = "[\\p{Alnum}][\\p{Alnum} ]*"; + + private String name; + private Price amount; + private LocalDate date; + private Category category; + + /** + * Constructor for Expense class. + * @param name Name of the expense + * @param amount Amount of the expense + * @param date Date of the expense + * @param category Category of the expense + */ + public Expense(String name, Price amount, LocalDate date, Category category) { + this.name = name; + this.amount = amount; + this.date = date; + this.category = category; + } + + /** + * Constructor for Expense class. + * @param name Name of the expense + * @param amount Amount of the expense + * @param date Date of the expense + * @param category Category of the expense + */ + public Expense(String name, double amount, LocalDate date, Category category) { + this.name = name; + this.amount = new Price(amount); + this.date = date; + this.category = category; + } + + /** + * Constructor for Expense class. + * @param name Name of the expense + * @param amount Amount of the expense + * @param date Date of the expense + * @param category Category of the expense + */ + public Expense(String name, String amount, LocalDate date, Category category) { + this.name = name; + this.amount = new Price(amount); + this.date = date; + this.category = category; + } + + public String getName() { + return name; + } + + public double getAmount() { + return amount.getPriceAsDouble(); + } + + public LocalDate getDate() { + return date; + } + + public String getFormattedDate() { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + return date.format(formatter); + } + + public Category getCategory() { + return category; + } + + @Override + public String toString() { + return "Name: " + name + + ", Amount: $" + amount + + ", Date: " + date + + ", Category: " + category; + } + + /** + * Returns true if a given string is a valid expense name. + * @param name Name to be tested + */ + public static boolean isValidName(String name) { + return name.matches(VALIDATION_REGEX); + } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Expense expense = (Expense) o; + return Objects.equals(name, expense.name) + && Objects.equals(amount, expense.amount) + && Objects.equals(date, expense.date) + && Objects.equals(category, expense.category); + } + + @Override + public int hashCode() { + int result; + long temp; + result = name != null ? name.hashCode() : 0; + temp = amount.hashCode(); + result = 31 * result + + (int) (temp ^ (temp >>> 32)); + result = 31 * result + + (date != null ? date.hashCode() : 0); + result = 31 * result + + (category != null ? category.hashCode() : 0); + return result; + } + + public void setName(String name) { + this.name = name; + } + + public void setAmount(String amount) { + this.amount = new Price(amount); + } + + public void setDate(LocalDate date) { + this.date = date; + } + + public void setCategory(Category category) { + this.category = category; + } + +} diff --git a/src/main/java/fasttrack/model/expense/ExpenseContainsKeywordsPredicate.java b/src/main/java/fasttrack/model/expense/ExpenseContainsKeywordsPredicate.java new file mode 100644 index 00000000000..b8e3729cc98 --- /dev/null +++ b/src/main/java/fasttrack/model/expense/ExpenseContainsKeywordsPredicate.java @@ -0,0 +1,32 @@ +package fasttrack.model.expense; + +import java.util.List; +import java.util.function.Predicate; + +import fasttrack.commons.util.StringUtil; + + +/** + * Tests that a {@code Expense}'s {@code Name} matches any of the keywords given. + */ +public class ExpenseContainsKeywordsPredicate implements Predicate { + private final List keywords; + + public ExpenseContainsKeywordsPredicate(List keywords) { + this.keywords = keywords; + } + + @Override + public boolean test(Expense expense) { + return keywords.stream() + .anyMatch(keyword -> StringUtil.containsWordIgnoreCase(expense.getName(), keyword)); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof ExpenseContainsKeywordsPredicate // instanceof handles nulls + && keywords.equals(((ExpenseContainsKeywordsPredicate) other).keywords)); // state check + } + +} diff --git a/src/main/java/fasttrack/model/expense/ExpenseInCategoryPredicate.java b/src/main/java/fasttrack/model/expense/ExpenseInCategoryPredicate.java new file mode 100644 index 00000000000..610763e44c6 --- /dev/null +++ b/src/main/java/fasttrack/model/expense/ExpenseInCategoryPredicate.java @@ -0,0 +1,37 @@ +package fasttrack.model.expense; + +import java.util.function.Predicate; + +import fasttrack.model.category.Category; + +/** + * Tests that a {@code Person}'s {@code Name} matches any of the keywords given. + */ +public class ExpenseInCategoryPredicate implements Predicate { + private final Category category; + + public ExpenseInCategoryPredicate(Category category) { + this.category = category; + } + + /** + * Get Category that this {@code ExpenseInCategoryPredicate} + * @return + */ + public Category getCategory() { + return this.category; + } + + @Override + public boolean test(Expense expense) { + return expense.getCategory().equals(this.category); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof ExpenseInCategoryPredicate // instanceof handles nulls + && category.equals(((ExpenseInCategoryPredicate) other).category)); // state check + } + +} diff --git a/src/main/java/fasttrack/model/expense/ExpenseInTimespanPredicate.java b/src/main/java/fasttrack/model/expense/ExpenseInTimespanPredicate.java new file mode 100644 index 00000000000..f8687eeab5e --- /dev/null +++ b/src/main/java/fasttrack/model/expense/ExpenseInTimespanPredicate.java @@ -0,0 +1,54 @@ +package fasttrack.model.expense; + +import java.time.LocalDate; +import java.util.function.Predicate; + +import fasttrack.logic.parser.ParserUtil; + +/** + * Tests that a {@code Person}'s {@code Name} matches any of the keywords given. + */ +public class ExpenseInTimespanPredicate implements Predicate { + private final ParserUtil.Timespan timespan; + private final LocalDate earliestDate; + + /** + * Creates an {@code ExpenseInTimespanPredicate} which returns true if the given date is after the earliest date + * in the timespan. + * @param timespan Timespan of week, month or year + */ + public ExpenseInTimespanPredicate(ParserUtil.Timespan timespan) { + this.timespan = timespan; + this.earliestDate = ParserUtil.getDateByTimespan(timespan); + } + + /** + * Creates an {@code ExpenseInTimespanPredicate} which returns true if the given date is after {@code earliestDate}. + * @param earliestDate LocalDate of the earliestDate this predicate will return True for + */ + public ExpenseInTimespanPredicate(LocalDate earliestDate) { + this.timespan = null; + this.earliestDate = earliestDate; + } + + @Override + public boolean test(Expense expense) { + return expense.getDate().isAfter(earliestDate) || expense.getDate().isEqual(earliestDate); + } + + /** + * Get Timespan that this {@code ExpenseInCategoryPredicate} is generated by. + * @return {@code Timespan} enum indicating week, month or year. + */ + public ParserUtil.Timespan getTimespan() { + return this.timespan; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof ExpenseInTimespanPredicate // instanceof handles nulls + && earliestDate.equals(((ExpenseInTimespanPredicate) other).earliestDate)); // state check + } + +} diff --git a/src/main/java/fasttrack/model/expense/ExpenseList.java b/src/main/java/fasttrack/model/expense/ExpenseList.java new file mode 100644 index 00000000000..35f0627be67 --- /dev/null +++ b/src/main/java/fasttrack/model/expense/ExpenseList.java @@ -0,0 +1,158 @@ +package fasttrack.model.expense; + +import static fasttrack.commons.util.CollectionUtil.requireAllNonNull; +import static java.util.Objects.requireNonNull; + +import java.util.Iterator; +import java.util.List; + +import fasttrack.model.category.Category; +import fasttrack.model.category.MiscellaneousCategory; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +/** + * A list of expenses that enforces uniqueness between its elements and + * does not allow nulls. + */ +public class ExpenseList implements Iterable { + + private final ObservableList internalListOfExpenses = FXCollections.observableArrayList(); + private final ObservableList internalUnmodifiableList = FXCollections + .unmodifiableObservableList(internalListOfExpenses); + private final Category misc = new MiscellaneousCategory(); + + /** + * Adds an expense to the internal list of expenses + * @param newExpense Expense to add + */ + public void add(Expense newExpense) { + requireNonNull(newExpense); + internalListOfExpenses.add(newExpense); + } + + /** + * Removes an expense from the internal list of expenses + * @param toRemove Expense to remove + */ + public void remove(Expense toRemove) { + requireNonNull(toRemove); + internalListOfExpenses.remove(toRemove); + } + + public void setExpense(Expense target, Expense editedExpense) { + requireAllNonNull(target, editedExpense); + int index = internalListOfExpenses.indexOf(target); + + internalListOfExpenses.set(index, editedExpense); + } + + public void set(int index, Expense newExpense) { + internalListOfExpenses.set(index, newExpense); + } + + /** + * Replace expenses with {@code target} category with Misc object + * @param target + */ + public void replaceDeletedCategory(Category target) { + requireNonNull(target); + internalListOfExpenses.forEach(expense -> { + if (expense.getCategory().equals(target)) { + expense.setCategory(misc); + } + }); + } + + /** + * Sets an internal list of expenses with a new list of expenses + * @param replacementList List of expenses to replace the current list + */ + public void setExpenseList(ExpenseList replacementList) { + requireNonNull(replacementList); + internalListOfExpenses.setAll(replacementList.internalListOfExpenses); + } + + /** + * Sets an internal list of expenses with a new list of expenses + * @param listOfExpenses List of expenses to replace the current list + */ + public void setExpenseList(List listOfExpenses) { + requireAllNonNull(listOfExpenses); + internalListOfExpenses.setAll(listOfExpenses); + } + + /** + * Returns the size of the internal list of expenses + * @return Size of the internal list of expenses + */ + public int getSize() { + return internalListOfExpenses.size(); + } + + /** + * Returns the total amount of the internal list of expenses + * @return Total amount of the internal list of expenses + */ + public double getTotalAmount() { + double totalAmount = 0; + for (Expense expense : internalListOfExpenses) { + totalAmount += expense.getAmount(); + } + return totalAmount; + } + + /** + * Delete all expense. + */ + public void clear() { + internalListOfExpenses.clear(); + internalUnmodifiableList.clear(); + } + + /** + * Sorts the internal list of expenses by date. + */ + public void sortList() { + internalListOfExpenses.sort((o1, o2) -> o1.getDate().isAfter(o2.getDate()) ? -1 + : o1.getDate().isEqual(o2.getDate()) ? 0 : 1); + } + + /** + * Returns the backing list as an unmodifiable {@code ObservableList}. + */ + public ObservableList asUnmodifiableList() { + return this.internalUnmodifiableList; + } + + @Override + public Iterator iterator() { + return this.internalListOfExpenses.iterator(); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof ExpenseList)) { + return false; + } + ExpenseList otherInUniqueList = (ExpenseList) other; + return this.internalListOfExpenses.equals(otherInUniqueList.internalListOfExpenses); + } + + /** + * Returns true if the list contains an equivalent person as the given argument. + * @param expense Expense to check + */ + public boolean contains(Expense expense) { + requireNonNull(expense); + return internalListOfExpenses.stream().anyMatch(expense::equals); + } + + @Override + public int hashCode() { + return internalListOfExpenses.hashCode(); + } +} diff --git a/src/main/java/fasttrack/model/expense/Price.java b/src/main/java/fasttrack/model/expense/Price.java new file mode 100644 index 00000000000..9eae0ed284e --- /dev/null +++ b/src/main/java/fasttrack/model/expense/Price.java @@ -0,0 +1,87 @@ +package fasttrack.model.expense; + +import static java.util.Objects.requireNonNull; + +import java.util.Objects; + +import fasttrack.logic.commands.exceptions.CommandException; + + + +/** + * Represents a Price of an Expense in FastTrack. + * Guarantees: immutable; is valid as declared in {@link #isValidPrice(String)} + */ +public class Price { + + public static final String MESSAGE_CONSTRAINTS = + "Amounts should only contain numbers, and should not be negative or empty."; + public static final String VALIDATION_REGEX = "^(0|\\d*)(\\.\\d+)?$"; + private String value; + + /** + * Constructs a {@code Price}. + * @param price A valid price. + */ + public Price(String price) { + requireNonNull(price); + if (!isValidPrice(price)) { + throw new IllegalArgumentException(MESSAGE_CONSTRAINTS); + } + value = price; + } + /** + * Constructs a {@code Price}. + * @param price A valid price. + */ + public Price(double price) { + requireNonNull(price); + if (!(price > -0.0)) { + throw new IllegalArgumentException(MESSAGE_CONSTRAINTS); + } + value = String.valueOf(price); + } + + /** + * Returns true if a given string is a valid price. + */ + public static boolean isValidPrice(String test) { + return test.matches(VALIDATION_REGEX) && Double.parseDouble(test) > -0; + } + + + public double getPriceAsDouble() { + return Double.parseDouble(value); + } + + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object other) { + Price otherTypecasted = (Price) other; + return other == this // short circuit if same object + || (other instanceof Price // instanceof handles nulls + && Objects.equals(getPriceAsDouble(), otherTypecasted.getPriceAsDouble())); // state check + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + public String getValue() { + return value; + } + + public void setValue(String price) throws CommandException { + if (!isValidPrice(price)) { + throw new CommandException(MESSAGE_CONSTRAINTS); + } + value = price; + } + +} + diff --git a/src/main/java/fasttrack/model/expense/RecurringExpenseList.java b/src/main/java/fasttrack/model/expense/RecurringExpenseList.java new file mode 100644 index 00000000000..090f4f206c5 --- /dev/null +++ b/src/main/java/fasttrack/model/expense/RecurringExpenseList.java @@ -0,0 +1,138 @@ +package fasttrack.model.expense; + +import static fasttrack.commons.util.CollectionUtil.requireAllNonNull; +import static java.util.Objects.requireNonNull; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; + +import fasttrack.model.category.Category; +import fasttrack.model.category.MiscellaneousCategory; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +/** + * Represents a List of Recurring Expenses in the Expense Tracker. + */ +public class RecurringExpenseList { + + private final ObservableList recurringExpenseList = FXCollections.observableArrayList(); + + private final ObservableList internalUnmodifiableList = FXCollections + .unmodifiableObservableList(recurringExpenseList); + + private final Category misc = new MiscellaneousCategory(); + + /** + * Adds a recurring expense to the internal list of recurring expenses. + * @param recurringExpense Recurring expense to add. + */ + public void addRecurringExpense(RecurringExpenseManager recurringExpense) { + recurringExpenseList.add(recurringExpense); + } + + + /** + * Removes a recurring expense from the internal list of recurring expenses. + * @param recurringExpense Recurring expense to remove. + */ + public void removeRecurringExpense(RecurringExpenseManager recurringExpense) { + recurringExpenseList.remove(recurringExpense); + } + + + public ObservableList getRecurringExpenseList() { + return recurringExpenseList; + } + + + /** + * Returns the backing list as an unmodifiable {@code ObservableList} + * @return The unmodifiable list. + */ + public ObservableList asUnmodifiableList() { + return this.internalUnmodifiableList; + } + + public ArrayList getExpenses() { + ArrayList expenses = new ArrayList<>(); + for (RecurringExpenseManager recurringExpense : recurringExpenseList) { + expenses.addAll(recurringExpense.getExpenses()); + } + return expenses; + } + + public void setRecurringExpenseList(RecurringExpenseList replacementList) { + recurringExpenseList.clear(); + recurringExpenseList.addAll(replacementList.getRecurringExpenseList()); + } + + public void setRecurringExpenseList(List replacementList) { + requireAllNonNull(replacementList); + recurringExpenseList.setAll(replacementList); + } + + /** + * Removes RecurringExpenseManager that are expired. + */ + public void cleanupExpiredGenerators() { + recurringExpenseList.removeIf((generator) -> { + if (generator.getExpenseEndDate() == null) { + return false; + } + return LocalDate.now().isAfter(generator.getExpenseEndDate()); + }); + } + + + public int getSize() { + return recurringExpenseList.size(); + } + + /** + * Returns true if the list contains an equivalent recurring expense as the given argument. + * @param recurringExpense Recurring expense to be compared to + * @return Boolean depicting if the recurring expense is present in the list. + */ + public boolean contains(RecurringExpenseManager recurringExpense) { + return recurringExpenseList.stream().anyMatch(recurringExpense::equals); + } + + public double getTotalAmount() { + double totalAmount = 0; + for (RecurringExpenseManager recurringExpense : recurringExpenseList) { + totalAmount += recurringExpense.getTotalAmount(); + } + return totalAmount; + } + + /** + * Delete all recurring expense. + */ + public void clear() { + recurringExpenseList.clear(); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + for (RecurringExpenseManager recurringExpense : recurringExpenseList) { + sb.append(recurringExpense.toString()); + } + return sb.toString(); + } + + /** + * Replace recurring expenses with {@code target} category with Misc object + * @param target + */ + public void replaceDeletedCategory(Category target) { + requireNonNull(target); + recurringExpenseList.forEach(recurringExpenseManager -> { + if (recurringExpenseManager.getExpenseCategory().equals(target)) { + recurringExpenseManager.setExpenseCategory(misc); + } + }); + } +} diff --git a/src/main/java/fasttrack/model/expense/RecurringExpenseManager.java b/src/main/java/fasttrack/model/expense/RecurringExpenseManager.java new file mode 100644 index 00000000000..9697b03f36a --- /dev/null +++ b/src/main/java/fasttrack/model/expense/RecurringExpenseManager.java @@ -0,0 +1,201 @@ +package fasttrack.model.expense; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Objects; + +import fasttrack.model.category.Category; + +/** + * Represents a Recurring Expense in the Expense Tracker. + */ +public class RecurringExpenseManager { + private String expenseName; + private Price amount; + private Category expenseCategory; + private int numberOfExpenses = 0; + private LocalDate nextExpenseDate = null; + private LocalDate startDate; + private LocalDate endDate = null; + private RecurringExpenseType recurringExpenseType; + + /** + * The constructor for the RecurringExpenseManager class with a start and end + * date. + * @param expenseName The name of the recurring expense. + * @param expenseAmount The amount of the recurring expense. + * @param expenseCategory The category of the recurring expense. + * @param startDate The start date of the recurring expense. + * @param endDate The end date of the recurring expense. + * @param recurringExpenseType The type of the recurring expense. + */ + public RecurringExpenseManager(String expenseName, Price expenseAmount, + Category expenseCategory, LocalDate startDate, LocalDate endDate, + RecurringExpenseType recurringExpenseType) { + this.expenseName = expenseName; + this.amount = expenseAmount; + this.expenseCategory = expenseCategory; + this.startDate = startDate; + this.endDate = endDate; + this.recurringExpenseType = recurringExpenseType; + this.nextExpenseDate = startDate; + } + + /** + * The constructor for the RecurringExpenseManager class with a start and end + * date. + * @param expenseName The name of the recurring expense. + * @param expenseAmount The amount of the recurring expense. + * @param expenseCategory The category of the recurring expense. + * @param startDate The start date of the recurring expense. + * @param endDate The end date of the recurring expense. + * @param recurringExpenseType The type of the recurring expense. + */ + public RecurringExpenseManager(String expenseName, double expenseAmount, + Category expenseCategory, LocalDate startDate, LocalDate endDate, + RecurringExpenseType recurringExpenseType) { + this.expenseName = expenseName; + this.amount = new Price(expenseAmount); + this.expenseCategory = expenseCategory; + this.startDate = startDate; + this.endDate = endDate; + this.recurringExpenseType = recurringExpenseType; + this.nextExpenseDate = startDate; + } + + /** + * The constructor for the RecurringExpenseManager class with no end date. + * @param expenseName The name of the recurring expense. + * @param amount The amount of the recurring expense. + * @param expenseCategory The category of the recurring expense. + * @param startDate The start date of the recurring expense. + * @param recurringExpenseType The type of the recurring expense. + */ + public RecurringExpenseManager(String expenseName, Price amount, + Category expenseCategory, LocalDate startDate, RecurringExpenseType recurringExpenseType) { + this.expenseName = expenseName; + this.amount = amount; + this.expenseCategory = expenseCategory; + this.startDate = startDate; + this.recurringExpenseType = recurringExpenseType; + this.nextExpenseDate = startDate; + } + + /** + * The constructor for the RecurringExpenseManager class with no end date. + * @param expenseName The name of the recurring expense. + * @param amount The amount of the recurring expense. + * @param expenseCategory The category of the recurring expense. + * @param startDate The start date of the recurring expense. + * @param recurringExpenseType The type of the recurring expense. + */ + public RecurringExpenseManager(String expenseName, double amount, + Category expenseCategory, LocalDate startDate, RecurringExpenseType recurringExpenseType) { + this.expenseName = expenseName; + this.amount = new Price(amount); + this.expenseCategory = expenseCategory; + this.startDate = startDate; + this.recurringExpenseType = recurringExpenseType; + this.nextExpenseDate = startDate; + } + + public ArrayList getExpenses() { + ArrayList expenses = new ArrayList<>(); + LocalDate newEndDate = LocalDate.now(); + if (endDate != null) { + newEndDate = !endDate.isAfter(LocalDate.now()) ? endDate : LocalDate.now(); + } + while (!nextExpenseDate.isAfter(newEndDate)) { + expenses.add(new Expense(expenseName, amount, nextExpenseDate, expenseCategory)); + nextExpenseDate = recurringExpenseType.getNextExpenseDate(nextExpenseDate); + } + numberOfExpenses = expenses.size(); + return expenses; + } + + public void setExpenseCategory(Category expenseCategory) { + this.expenseCategory = expenseCategory; + } + + public void setExpenseName(String expenseName) { + this.expenseName = expenseName; + } + + public void setAmount(String expenseAmount) { + this.amount = new Price(expenseAmount); + } + + public void setAmount(double expenseAmount) { + this.amount = new Price(expenseAmount); + } + + public void setEndDate(LocalDate endDate) { + this.endDate = endDate; + } + + public void setRecurringExpenseType(RecurringExpenseType recurringExpenseType) { + this.recurringExpenseType = recurringExpenseType; + } + + public int getNumberOfExpenses() { + return numberOfExpenses; + } + + public double getTotalAmount() { + return amount.getPriceAsDouble() * numberOfExpenses; + } + + public LocalDate getNextExpenseDate() { + return nextExpenseDate; + } + + public LocalDate getExpenseStartDate() { + return startDate; + } + + public RecurringExpenseType getRecurringExpenseType() { + return recurringExpenseType; + } + + public LocalDate getExpenseEndDate() { + return endDate; + } + + public String getExpenseName() { + return expenseName; + } + + public Category getExpenseCategory() { + return expenseCategory; + } + + public double getAmount() { + return amount.getPriceAsDouble(); + } + + public void setNextExpenseDate(LocalDate nextExpenseDate) { + this.nextExpenseDate = nextExpenseDate; + } + + @Override + public String toString() { + String endStatus = endDate == null ? "Ongoing" : String.valueOf(endDate); + return "Recurring Expense: " + expenseName + ", Amount: " + amount + ", Category: " + + expenseCategory + ", Start Date: " + startDate + ", End Date: " + endStatus + + ", Recurring Expense Type: " + recurringExpenseType; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof RecurringExpenseManager) { + RecurringExpenseManager other = (RecurringExpenseManager) obj; + return this.expenseName.equals(other.expenseName) + && this.amount.equals(other.amount) + && this.expenseCategory.equals(other.expenseCategory) + && this.startDate.equals(other.startDate) + && Objects.equals(endDate, other.endDate) + && this.recurringExpenseType.equals(other.recurringExpenseType); + } + return false; + } +} diff --git a/src/main/java/fasttrack/model/expense/RecurringExpenseType.java b/src/main/java/fasttrack/model/expense/RecurringExpenseType.java new file mode 100644 index 00000000000..0e1109dd5ff --- /dev/null +++ b/src/main/java/fasttrack/model/expense/RecurringExpenseType.java @@ -0,0 +1,37 @@ +package fasttrack.model.expense; + +import java.time.LocalDate; +/** + * Enum for Recurring Expense Type. + */ +public enum RecurringExpenseType { + MONTHLY, + WEEKLY, + DAILY, + YEARLY; + + /** + * Returns the next expense date based on the recurring expense type. + * @param currentDate The current date. + * @return The next expense date. + */ + public LocalDate getNextExpenseDate(LocalDate currentDate) { + switch (this) { + case MONTHLY: + currentDate = currentDate.plusMonths(1); + break; + case WEEKLY: + currentDate = currentDate.plusWeeks(1); + break; + case DAILY: + currentDate = currentDate.plusDays(1); + break; + case YEARLY: + currentDate = currentDate.plusYears(1); + break; + default: + break; + } + return currentDate; + } +} diff --git a/src/main/java/fasttrack/model/util/AnalyticsType.java b/src/main/java/fasttrack/model/util/AnalyticsType.java new file mode 100644 index 00000000000..87601844f7d --- /dev/null +++ b/src/main/java/fasttrack/model/util/AnalyticsType.java @@ -0,0 +1,23 @@ +package fasttrack.model.util; + +/** + * Represents different types of analytics that can be calculated in FastTrack + * MONTHLY_SPENT: Represents the total amount spent in a month. + * MONTHLY_REMAINING: Represents the remaining budget for the month. + * WEEKLY_SPENT: Represents the total amount spent in a week. + * WEEKLY_REMAINING: Represents the remaining budget for the week. + * WEEKLY_CHANGE: Represents the change in spending from the previous week. + * MONTHLY_CHANGE: Represents the change in spending from the previous month. + * TOTAL_SPENT: Represents the total amount spent overall. + * BUDGET_PERCENTAGE: Represents the percentage of the budget that has been spent. + */ +public enum AnalyticsType { + MONTHLY_SPENT, + MONTHLY_REMAINING, + WEEKLY_SPENT, + WEEKLY_REMAINING, + WEEKLY_CHANGE, + MONTHLY_CHANGE, + TOTAL_SPENT, + BUDGET_PERCENTAGE +} diff --git a/src/main/java/fasttrack/model/util/CommandUtility.java b/src/main/java/fasttrack/model/util/CommandUtility.java new file mode 100644 index 00000000000..35d31b2f6d1 --- /dev/null +++ b/src/main/java/fasttrack/model/util/CommandUtility.java @@ -0,0 +1,40 @@ +package fasttrack.model.util; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; + +/** + * Utility class which contains helper functions related to various commands, such as conversion of date and times + */ +public class CommandUtility { + + /** + * Parses a date string input in any allowed format and returns a LocalDate object. + * @param input A date string of any allowed format + * @return A LocalDate object representing the parsed date + * @throws IllegalArgumentException If the input string cannot be parsed into a valid date + */ + public static LocalDate parseDateFromUserInput(String input) throws IllegalArgumentException { + DateTimeFormatter[] formatters = { + DateTimeFormatter.ofPattern("dd/MM/yy"), + DateTimeFormatter.ofPattern("dd/MM/yyyy"), + DateTimeFormatter.ofPattern("d/M/yy"), + DateTimeFormatter.ofPattern("d/M/yyyy"), + DateTimeFormatter.ofPattern("d/MM/yy"), + DateTimeFormatter.ofPattern("d/MM/yyyy"), + DateTimeFormatter.ofPattern("dd/M/yy"), + DateTimeFormatter.ofPattern("dd/M/yyyy") + }; + for (DateTimeFormatter formatter : formatters) { + try { + return LocalDate.parse(input, formatter); + } catch (DateTimeParseException ignored) { + continue; + } + } + throw new IllegalArgumentException("Invalid date format"); + } + +} + diff --git a/src/main/java/fasttrack/model/util/SampleExpenseTracker.java b/src/main/java/fasttrack/model/util/SampleExpenseTracker.java new file mode 100644 index 00000000000..a72a85c4d18 --- /dev/null +++ b/src/main/java/fasttrack/model/util/SampleExpenseTracker.java @@ -0,0 +1,66 @@ +package fasttrack.model.util; + +import java.time.LocalDate; + +import fasttrack.model.Budget; +import fasttrack.model.ExpenseTracker; +import fasttrack.model.ReadOnlyExpenseTracker; +import fasttrack.model.category.Category; +import fasttrack.model.category.UserDefinedCategory; +import fasttrack.model.expense.Expense; + +/** + * Class containing sample data for ExpenseTracker to initialize if needed. + */ +public class SampleExpenseTracker { + + private static final Category food = new UserDefinedCategory("Food", "For food"); + private static final Category entertainment = new UserDefinedCategory("Entertainment", "For entertainment"); + private static final Category transportation = new UserDefinedCategory("Transportation", "For bus, car, train"); + private static final Category shopping = new UserDefinedCategory("Shopping", ""); + private static final Category housing = new UserDefinedCategory("Housing", ""); + /** + * Sample data for categories + * @return an array of sample categories. + */ + public static Category[] getSampleCategories() { + return new Category[] { + food, entertainment, transportation, shopping, housing + }; + } + + /** + * Sample data for expenses + * @return an array of sample expenses. + */ + public static Expense[] getSampleExpenses() { + return new Expense[] { + new Expense("Meal at JE", "4.50", LocalDate.now(), food), + new Expense("Movie ticket", "12.99", LocalDate.of(2023, 3, 15), entertainment), + new Expense("MRT fare", "45.80", LocalDate.of(2023, 3, 10), transportation), + new Expense("Shoes", "75.00", LocalDate.of(2023, 3, 20), shopping), + new Expense("Groceries", "56.30", LocalDate.of(2023, 3, 25), food) + }; + } + + /** + * Sets all required sample data for categories and expenses. + * @return ReadOnlyExpenseTracker to be read from. + */ + public static ReadOnlyExpenseTracker getSampleExpenseTracker() { + ExpenseTracker sampleExpenseTracker = new ExpenseTracker(); + for (Category sampleCategory : getSampleCategories()) { + sampleExpenseTracker.addCategory(sampleCategory); + } + + for (Expense sampleExpense : getSampleExpenses()) { + if (sampleExpenseTracker.getCategoryInstance(sampleExpense.getCategory()) != null) { + sampleExpense.setCategory(sampleExpenseTracker.getCategoryInstance(sampleExpense.getCategory())); + } + sampleExpenseTracker.addExpense(sampleExpense); + } + + sampleExpenseTracker.setBudget(new Budget(0)); + return sampleExpenseTracker; + } +} diff --git a/src/main/java/fasttrack/model/util/StorageUtility.java b/src/main/java/fasttrack/model/util/StorageUtility.java new file mode 100644 index 00000000000..31e4ad28b74 --- /dev/null +++ b/src/main/java/fasttrack/model/util/StorageUtility.java @@ -0,0 +1,22 @@ +package fasttrack.model.util; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + + +/** + * Utility class which contains helper functions related to various commands, such as conversion of date and times + */ +public class StorageUtility { + + /** + * Parses a date string in the format of "yyyy-MM-dd" from JSON to a LocalDate object. + * @param dateString the date string to parse + * @return a LocalDate object representing the parsed date + */ + public static LocalDate parseDateFromJson(String dateString) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + return LocalDate.parse(dateString, formatter); + } +} + diff --git a/src/main/java/fasttrack/model/util/UserInterfaceUtil.java b/src/main/java/fasttrack/model/util/UserInterfaceUtil.java new file mode 100644 index 00000000000..d8b29e95b24 --- /dev/null +++ b/src/main/java/fasttrack/model/util/UserInterfaceUtil.java @@ -0,0 +1,45 @@ +package fasttrack.model.util; + +import java.text.DecimalFormat; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +/** + * Utility class which contains functions for utility purposes, such as conversion of dates and prices + * @author Nicholas Lee + */ +public class UserInterfaceUtil { + + /** + * Returns a string representation of the given date object in the format "dd/MM/yy". + * @param date the date object to be formatted + * @return a formatted date string + */ + public static String parseDate(LocalDate date) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yy"); + return date.format(formatter); + } + + /** + * Returns a string representation of the given double as a price string in the format "$xx.xx". + * @param amount double value representing a price + * @return a formatted price string representation + */ + public static String parsePrice(double amount) { + DecimalFormat df = new DecimalFormat("#0.00"); + String priceString = df.format(amount); + return "$" + priceString; + } + + + /** + * Returns the input string with the first letter capitalized and the rest of the letters in lower case. + * @param input the string to be capitalized + * @return a string with the first letter capitalized + */ + public static String capitalizeFirstLetter(final String input) { + return Character.toUpperCase(input.charAt(0)) + input.substring(1); + } +} + + diff --git a/src/main/java/fasttrack/storage/ExpenseTrackerStorage.java b/src/main/java/fasttrack/storage/ExpenseTrackerStorage.java new file mode 100644 index 00000000000..50a6906c0f5 --- /dev/null +++ b/src/main/java/fasttrack/storage/ExpenseTrackerStorage.java @@ -0,0 +1,45 @@ +package fasttrack.storage; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Optional; + +import fasttrack.commons.exceptions.DataConversionException; +import fasttrack.model.ReadOnlyExpenseTracker; + +/** + * Represents a storage for {@link fasttrack.model.ExpenseTracker}. + */ +public interface ExpenseTrackerStorage { + + /** + * Returns the file path of the data file. + */ + Path getExpenseTrackerFilePath(); + + /** + * Returns ExpenseTracker data as a {@link ReadOnlyExpenseTracker}. + * Returns {@code Optional.empty()} if storage file is not found. + * @throws DataConversionException if the data in storage is not in the expected format. + * @throws IOException if there was any problem when reading from the storage. + */ + Optional readExpenseTracker() throws DataConversionException, IOException; + + /** + * @see #getExpenseTrackerFilePath() + */ + Optional readExpenseTracker(Path filePath) throws DataConversionException, IOException; + + /** + * Saves the given {@link ReadOnlyExpenseTracker} to the storage. + * @param expenseTracker cannot be null. + * @throws IOException if there was any problem writing to the file. + */ + void saveExpenseTracker(ReadOnlyExpenseTracker expenseTracker) throws IOException; + + /** + * @see #saveExpenseTracker(ReadOnlyExpenseTracker) + */ + void saveExpenseTracker(ReadOnlyExpenseTracker expenseTracker, Path filePath) throws IOException; + +} diff --git a/src/main/java/fasttrack/storage/JsonAdaptedBudget.java b/src/main/java/fasttrack/storage/JsonAdaptedBudget.java new file mode 100644 index 00000000000..c552e66b856 --- /dev/null +++ b/src/main/java/fasttrack/storage/JsonAdaptedBudget.java @@ -0,0 +1,48 @@ +package fasttrack.storage; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import fasttrack.commons.exceptions.IllegalValueException; +import fasttrack.model.Budget; + + +/** + * Jackson-friendly version of {@link Budget}. + */ +class JsonAdaptedBudget { + + public static final String MISSING_FIELD_MESSAGE_FORMAT = "Budget's %s field is missing!"; + private final String amount; + + /** + * Constructs a {@code JsonAdaptedBudget} with the given budget details. + */ + @JsonCreator + public JsonAdaptedBudget(@JsonProperty("amount") String amount) { + this.amount = amount; + } + + /** + * Converts a given {@code Budget} into this class for Jackson use. + */ + public JsonAdaptedBudget(Budget source) { + this.amount = source.toString(); + } + + /** + * Converts this Jackson-friendly adapted budget object into the model's + * {@code Budget} object. + * @throws IllegalValueException if there were any data constraints violated + * in the adapted budget. + */ + public Budget toModelType() throws IllegalValueException { + + if (amount == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT)); + } + + return new Budget(Double.parseDouble(amount)); + } + +} diff --git a/src/main/java/fasttrack/storage/JsonAdaptedCategory.java b/src/main/java/fasttrack/storage/JsonAdaptedCategory.java new file mode 100644 index 00000000000..e59303f483e --- /dev/null +++ b/src/main/java/fasttrack/storage/JsonAdaptedCategory.java @@ -0,0 +1,65 @@ +package fasttrack.storage; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import fasttrack.commons.exceptions.IllegalValueException; +import fasttrack.model.category.Category; +import fasttrack.model.category.MiscellaneousCategory; +import fasttrack.model.category.UserDefinedCategory; + + + +/** + * Jackson-friendly version of {@link Category}. + */ +class JsonAdaptedCategory { + + public static final String MISSING_FIELD_MESSAGE_FORMAT = "Category's %s field is missing!"; + private final String categoryName; + private final String summary; + + /** + * Constructs a {@code JsonAdaptedCategory} with the given category details. + */ + @JsonCreator + public JsonAdaptedCategory(@JsonProperty("categoryName") String categoryName, + @JsonProperty("summary") String summary) { + this.categoryName = categoryName; + this.summary = summary; + } + + /** + * Converts a given {@code Category} into this class for Jackson use. + */ + public JsonAdaptedCategory(Category source) { + this.categoryName = source.getCategoryName(); + this.summary = source.getSummary(); + } + + /** + * Converts this Jackson-friendly adapted category object into the model's + * {@code Category} object. + * @throws IllegalValueException if there were any data constraints violated + * in the adapted category. + */ + public Category toModelType() throws IllegalValueException { + + if (categoryName == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, "Category")); + } + + final String modelCategoryName = categoryName; + if (summary == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, "Summary")); + } + + final String modelDescription = summary; + + if (categoryName.equalsIgnoreCase("misc")) { + return new MiscellaneousCategory(); + } + return new UserDefinedCategory(modelCategoryName, modelDescription); + } + +} diff --git a/src/main/java/fasttrack/storage/JsonAdaptedExpense.java b/src/main/java/fasttrack/storage/JsonAdaptedExpense.java new file mode 100644 index 00000000000..a574bf05f5d --- /dev/null +++ b/src/main/java/fasttrack/storage/JsonAdaptedExpense.java @@ -0,0 +1,81 @@ +package fasttrack.storage; + +import java.time.LocalDate; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import fasttrack.commons.exceptions.IllegalValueException; +import fasttrack.model.category.Category; +import fasttrack.model.expense.Expense; +import fasttrack.model.util.StorageUtility; + +/** + * Jackson-friendly version of {@link Expense}. + */ +public class JsonAdaptedExpense { + + public static final String MISSING_FIELD_MESSAGE_FORMAT = "Expense's %s field is missing!"; + + private final String name; + private final String amount; + private final String date; + private final JsonAdaptedCategory category; + + + + /** + * Constructs a {@code JsonAdaptedExpense} with the given expense details. + * @param name Name of the expense. + * @param amount Amount of the expense. + * @param date Date of the expense. + * @param category Category of the expense. + */ + @JsonCreator + public JsonAdaptedExpense(@JsonProperty("name") String name, @JsonProperty("amount") String amount, + @JsonProperty("date") String date, + @JsonProperty("category") JsonAdaptedCategory category) { + this.name = name; + this.amount = amount; + this.category = category; + this.date = date; + } + + /** + * Converts a given {@code Expense} into this class for Jackson use. + * https://stackoverflow.com/questions/530012/how-to-convert-java-util-date-to-java-sql-date + * @param source future changes to this will not affect the created {@code JsonAdaptedExpense}. + */ + public JsonAdaptedExpense(Expense source) { + name = source.getName(); + amount = Double.toString(source.getAmount()); + date = source.getFormattedDate(); + category = new JsonAdaptedCategory( + source.getCategory().getCategoryName(), source.getCategory().getSummary()); + } + + /** + * Converts this Jackson-friendly adapted expense object into the model's {@code Expense} object. + * @throws IllegalValueException if there were any data constraints violated in the adapted expense. + */ + public Expense toModelType() throws IllegalValueException { + if (name == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, "Name")); + } + if (!Expense.isValidName(name)) { + throw new IllegalValueException(Expense.MESSAGE_CONSTRAINTS); + } + + final String modelName = name; + + if (date == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, "Date")); + } + + final LocalDate modelDate = StorageUtility.parseDateFromJson(date); + + Category modelCategory = category.toModelType(); + + return new Expense(modelName, amount, modelDate, modelCategory); + } +} diff --git a/src/main/java/fasttrack/storage/JsonAdaptedRecurringExpenseManager.java b/src/main/java/fasttrack/storage/JsonAdaptedRecurringExpenseManager.java new file mode 100644 index 00000000000..8cd73cf633a --- /dev/null +++ b/src/main/java/fasttrack/storage/JsonAdaptedRecurringExpenseManager.java @@ -0,0 +1,127 @@ +package fasttrack.storage; + +import java.time.LocalDate; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import fasttrack.commons.exceptions.IllegalValueException; +import fasttrack.model.category.Category; +import fasttrack.model.expense.Price; +import fasttrack.model.expense.RecurringExpenseManager; +import fasttrack.model.expense.RecurringExpenseType; +import fasttrack.model.util.StorageUtility; + +/** + * Jackson-friendly version of {@link RecurringExpenseManager}. + */ +public class JsonAdaptedRecurringExpenseManager { + public static final String MISSING_FIELD_MESSAGE_FORMAT = "Recurring Expense Manager's field is missing!"; + + private final String expenseName; + private final String expenseAmount; + private final JsonAdaptedCategory expenseCategory; + private final String nextExpenseDate; + private final String startDate; + private final String endDate; + private final String recurringExpenseType; + + /** + * Constructs a {@code JsonAdaptedRecurringExpenseManager} with the given + * category details. + * @param expenseName Name of the expense. + * @param expenseAmount Amount of the expense. + * @param expenseCategory Category of the expense. + * @param nextExpenseDate The next date at which the expense will be + * charged. + * @param startDate The starting date at which the recurring expense + * was first added. + * @param endDate The ending date at which the recurring expense + * will end. + * @param recurringExpenseType Frequency-interval of which the expense will be + * added. + */ + @JsonCreator + public JsonAdaptedRecurringExpenseManager(@JsonProperty("expenseName") String expenseName, + @JsonProperty("expenseAmount") String expenseAmount, + @JsonProperty("expenseCategory") JsonAdaptedCategory expenseCategory, + @JsonProperty("nextExpenseDate") String nextExpenseDate, + @JsonProperty("startDate") String startDate, + @JsonProperty("endDate") String endDate, + @JsonProperty("recurringExpenseType") String recurringExpenseType) { + this.expenseName = expenseName; + this.expenseAmount = expenseAmount; + this.expenseCategory = expenseCategory; + this.nextExpenseDate = nextExpenseDate; + this.startDate = startDate; + this.endDate = endDate; + this.recurringExpenseType = recurringExpenseType; + } + + /** + * Converts a given {@code RecurringExpenseManager} into this class for Jackson + * use. + * @param source future changes to this will not affect the created + * {@code JsonAdaptedRecurringExpenseManager} + */ + public JsonAdaptedRecurringExpenseManager(RecurringExpenseManager source) { + this.expenseName = source.getExpenseName(); + this.expenseAmount = Double.toString(source.getAmount()); + this.expenseCategory = new JsonAdaptedCategory(source.getExpenseCategory().getCategoryName(), + source.getExpenseCategory().getSummary()); + this.nextExpenseDate = String.valueOf(source.getNextExpenseDate()); + this.startDate = String.valueOf(source.getExpenseStartDate()); + this.endDate = String.valueOf(source.getExpenseEndDate()); + this.recurringExpenseType = String.valueOf(source.getRecurringExpenseType()); + } + + /** + * Converts this Jackson-friendly adapted RecurringExpenseManager object into + * the model's + * {@code RecurringExpenseManager} object. + * @throws IllegalValueException if there were any data constraints violated + * in the adapted category. + */ + public RecurringExpenseManager toModelType() throws IllegalValueException { + if (expenseName == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT)); + } + + if (expenseAmount == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT)); + } + + if (expenseCategory == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT)); + } + + if (startDate == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT)); + } + + if (recurringExpenseType == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT)); + } + + LocalDate modelStartDate = StorageUtility.parseDateFromJson(startDate); + + LocalDate modelNextExpenseDate = StorageUtility.parseDateFromJson(nextExpenseDate); + + RecurringExpenseType modelRecurringType = RecurringExpenseType.valueOf(recurringExpenseType); + + Category toBeUsed = expenseCategory.toModelType(); + Price amount = new Price(expenseAmount); + if (!endDate.equals("null")) { + LocalDate modelEndDate = StorageUtility.parseDateFromJson(endDate); + RecurringExpenseManager toReturn = new RecurringExpenseManager(expenseName, amount, + toBeUsed, modelStartDate, modelEndDate, modelRecurringType); + toReturn.setNextExpenseDate(modelNextExpenseDate); + return toReturn; + } + + RecurringExpenseManager toReturn = new RecurringExpenseManager(expenseName, amount, + toBeUsed, modelStartDate, modelRecurringType); + toReturn.setNextExpenseDate(modelNextExpenseDate); + return toReturn; + } +} diff --git a/src/main/java/fasttrack/storage/JsonExpenseTrackerStorage.java b/src/main/java/fasttrack/storage/JsonExpenseTrackerStorage.java new file mode 100644 index 00000000000..881d314fe8b --- /dev/null +++ b/src/main/java/fasttrack/storage/JsonExpenseTrackerStorage.java @@ -0,0 +1,76 @@ +package fasttrack.storage; + +import static java.util.Objects.requireNonNull; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Optional; +import java.util.logging.Logger; + +import fasttrack.commons.core.LogsCenter; +import fasttrack.commons.exceptions.DataConversionException; +import fasttrack.commons.exceptions.IllegalValueException; +import fasttrack.commons.util.FileUtil; +import fasttrack.commons.util.JsonUtil; +import fasttrack.model.ReadOnlyExpenseTracker; + +/** + * A class to access ExpenseTracker data stored as a json file on the hard disk. + */ +public class JsonExpenseTrackerStorage implements ExpenseTrackerStorage { + + private static final Logger logger = LogsCenter.getLogger(JsonExpenseTrackerStorage.class); + private Path filePath; + + public JsonExpenseTrackerStorage(Path filePath) { + this.filePath = filePath; + } + + public Path getExpenseTrackerFilePath() { + return filePath; + } + + @Override + public Optional readExpenseTracker() throws DataConversionException { + return readExpenseTracker(filePath); + } + + /** + * Similar to {@link #readExpenseTracker()}. + * @param filePath location of the data. Cannot be null. + * @throws DataConversionException if the file is not in the correct format. + */ + public Optional readExpenseTracker(Path filePath) throws DataConversionException { + requireNonNull(filePath); + + Optional jsonExpenseTracker = JsonUtil.readJsonFile( + filePath, JsonSerializableExpenseTracker.class); + if (!jsonExpenseTracker.isPresent()) { + return Optional.empty(); + } + + try { + return Optional.of(jsonExpenseTracker.get().toModelType()); + } catch (IllegalValueException ive) { + logger.info("Illegal values found in " + filePath + ": " + ive.getMessage()); + throw new DataConversionException(ive); + } + } + + @Override + public void saveExpenseTracker(ReadOnlyExpenseTracker expenseTracker) throws IOException { + saveExpenseTracker(expenseTracker, filePath); + } + + /** + * Similar to {@link #saveExpenseTracker(ReadOnlyExpenseTracker)}. + * @param filePath location of the data. Cannot be null. + */ + public void saveExpenseTracker(ReadOnlyExpenseTracker expenseTracker, Path filePath) throws IOException { + requireNonNull(expenseTracker); + requireNonNull(filePath); + FileUtil.createIfMissing(filePath); + JsonUtil.saveJsonFile(new JsonSerializableExpenseTracker(expenseTracker), filePath); + } + +} diff --git a/src/main/java/fasttrack/storage/JsonSerializableExpenseTracker.java b/src/main/java/fasttrack/storage/JsonSerializableExpenseTracker.java new file mode 100644 index 00000000000..0ccf0245f61 --- /dev/null +++ b/src/main/java/fasttrack/storage/JsonSerializableExpenseTracker.java @@ -0,0 +1,113 @@ +package fasttrack.storage; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonRootName; + +import fasttrack.commons.exceptions.IllegalValueException; +import fasttrack.model.ExpenseTracker; +import fasttrack.model.ReadOnlyExpenseTracker; +import fasttrack.model.category.Category; +import fasttrack.model.category.MiscellaneousCategory; +import fasttrack.model.expense.Expense; +import fasttrack.model.expense.RecurringExpenseManager; + +/** + * An Immutable ExpenseTracker that is serializable to JSON format. + */ +@JsonRootName(value = "expenseTracker") +class JsonSerializableExpenseTracker { + + private final List categories = new ArrayList<>(); + private final List expenses = new ArrayList<>(); + private final List recurringGenerators = new ArrayList<>(); + private final JsonAdaptedBudget budget; + + /** + * Constructs a {@code JsonSerializableExpenseTracker} with the given expenses + * and categories. + * @param listOfCategories list of categories to be added to the ExpenseTracker + * @param listOfExpenses list of expenses to be added to the ExpenseTracker + */ + @JsonCreator + public JsonSerializableExpenseTracker(@JsonProperty("categories") List listOfCategories, + @JsonProperty("expenses") List listOfExpenses, + @JsonProperty("budget") JsonAdaptedBudget budget, + @JsonProperty("recurringGenerators") List recurringGenerators) { + this.categories.addAll(listOfCategories); + this.expenses.addAll(listOfExpenses); + this.budget = budget; + this.recurringGenerators.addAll(recurringGenerators); + } + + /** + * Converts a given {@code ReadOnlyExpenseTracker} into this class for Jackson + * use. + * @param source future changes to this will not affect the created + * {@code JsonSerializableExpenseTracker}. + */ + public JsonSerializableExpenseTracker(ReadOnlyExpenseTracker source) { + this.categories.addAll(source.getCategoryList() + .stream().map(JsonAdaptedCategory::new).collect(Collectors.toList())); + this.expenses.addAll(source.getExpenseList() + .stream().map(JsonAdaptedExpense::new).collect(Collectors.toList())); + this.budget = new JsonAdaptedBudget(source.getBudget()); + this.recurringGenerators.addAll(source.getRecurringExpenseGenerators() + .stream().map(JsonAdaptedRecurringExpenseManager::new).collect(Collectors.toList())); + } + + /** + * Converts this ExpenseTracker into the model's {@code ExpenseTracker} object. + * @throws IllegalValueException if there were any data constraints violated. + */ + public ExpenseTracker toModelType() throws IllegalValueException { + ExpenseTracker expenseTracker = new ExpenseTracker(); + + for (JsonAdaptedCategory jsonAdaptedCategory : categories) { + Category category = jsonAdaptedCategory.toModelType(); + expenseTracker.addCategory(category); + } + + for (JsonAdaptedRecurringExpenseManager jsonAdaptedGenerator : recurringGenerators) { + RecurringExpenseManager expenseGenerator = jsonAdaptedGenerator.toModelType(); + Category associatedCategory = getAssociatedCategoryForRecurring(expenseGenerator, expenseTracker); + if (associatedCategory == null) { + if (!(expenseGenerator.getExpenseCategory() instanceof MiscellaneousCategory)) { + expenseTracker.addCategory(expenseGenerator.getExpenseCategory()); + } + } else { + expenseGenerator.setExpenseCategory(associatedCategory); + } + expenseTracker.addRecurringGenerator(expenseGenerator); + } + + for (JsonAdaptedExpense jsonAdaptedExpense : expenses) { + Expense expense = jsonAdaptedExpense.toModelType(); + Category associatedCategory = getAssociatedCategory(expense, expenseTracker); + if (associatedCategory == null) { + if (!(expense.getCategory() instanceof MiscellaneousCategory)) { + expenseTracker.addCategory(expense.getCategory()); + } + } else { + expense.setCategory(associatedCategory); + } + expenseTracker.addExpense(expense); + } + + expenseTracker.setBudget(budget.toModelType()); + + return expenseTracker; + } + + private Category getAssociatedCategory(Expense expense, ExpenseTracker expenseTracker) { + return expenseTracker.getCategoryInstance(expense.getCategory()); + } + + private Category getAssociatedCategoryForRecurring(RecurringExpenseManager recur, ExpenseTracker expenseTracker) { + return expenseTracker.getCategoryInstance(recur.getExpenseCategory()); + } +} diff --git a/src/main/java/seedu/address/storage/JsonUserPrefsStorage.java b/src/main/java/fasttrack/storage/JsonUserPrefsStorage.java similarity index 83% rename from src/main/java/seedu/address/storage/JsonUserPrefsStorage.java rename to src/main/java/fasttrack/storage/JsonUserPrefsStorage.java index bc2bbad84aa..eb89bdcd9cd 100644 --- a/src/main/java/seedu/address/storage/JsonUserPrefsStorage.java +++ b/src/main/java/fasttrack/storage/JsonUserPrefsStorage.java @@ -1,13 +1,13 @@ -package seedu.address.storage; +package fasttrack.storage; import java.io.IOException; import java.nio.file.Path; import java.util.Optional; -import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.commons.util.JsonUtil; -import seedu.address.model.ReadOnlyUserPrefs; -import seedu.address.model.UserPrefs; +import fasttrack.commons.exceptions.DataConversionException; +import fasttrack.commons.util.JsonUtil; +import fasttrack.model.ReadOnlyUserPrefs; +import fasttrack.model.UserPrefs; /** * A class to access UserPrefs stored in the hard disk as a json file diff --git a/src/main/java/fasttrack/storage/Storage.java b/src/main/java/fasttrack/storage/Storage.java new file mode 100644 index 00000000000..8f18507031b --- /dev/null +++ b/src/main/java/fasttrack/storage/Storage.java @@ -0,0 +1,32 @@ +package fasttrack.storage; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Optional; + +import fasttrack.commons.exceptions.DataConversionException; +import fasttrack.model.ReadOnlyExpenseTracker; +import fasttrack.model.ReadOnlyUserPrefs; +import fasttrack.model.UserPrefs; + +/** + * API of the Storage component + */ +public interface Storage extends ExpenseTrackerStorage, UserPrefsStorage { + + @Override + Optional readUserPrefs() throws DataConversionException, IOException; + + @Override + void saveUserPrefs(ReadOnlyUserPrefs userPrefs) throws IOException; + + @Override + Path getExpenseTrackerFilePath(); + + @Override + Optional readExpenseTracker() throws DataConversionException, IOException; + + @Override + void saveExpenseTracker(ReadOnlyExpenseTracker expenseTracker) throws IOException; + +} diff --git a/src/main/java/fasttrack/storage/StorageManager.java b/src/main/java/fasttrack/storage/StorageManager.java new file mode 100644 index 00000000000..9da84ece82e --- /dev/null +++ b/src/main/java/fasttrack/storage/StorageManager.java @@ -0,0 +1,78 @@ +package fasttrack.storage; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Optional; +import java.util.logging.Logger; + +import fasttrack.commons.core.LogsCenter; +import fasttrack.commons.exceptions.DataConversionException; +import fasttrack.model.ReadOnlyExpenseTracker; +import fasttrack.model.ReadOnlyUserPrefs; +import fasttrack.model.UserPrefs; + +/** + * Manages storage of ExpenseTracker data in local storage. + */ +public class StorageManager implements Storage { + + private static final Logger logger = LogsCenter.getLogger(StorageManager.class); + private final ExpenseTrackerStorage expenseBookStorage; + private final UserPrefsStorage userPrefsStorage; + + /** + * Creates a {@code StorageManager} with the given {@code ExpenseTrackerStorage} + * and {@code UserPrefStorage}. + */ + public StorageManager(ExpenseTrackerStorage expenseTracker, UserPrefsStorage userPrefsStorage) { + this.expenseBookStorage = expenseTracker; + this.userPrefsStorage = userPrefsStorage; + } + + // ================ UserPrefs methods ============================== + + @Override + public Path getUserPrefsFilePath() { + return userPrefsStorage.getUserPrefsFilePath(); + } + + @Override + public Optional readUserPrefs() throws DataConversionException, IOException { + return userPrefsStorage.readUserPrefs(); + } + + @Override + public void saveUserPrefs(ReadOnlyUserPrefs userPrefs) throws IOException { + userPrefsStorage.saveUserPrefs(userPrefs); + } + + // ================ ExpenseTracker methods ============================== + + @Override + public Path getExpenseTrackerFilePath() { + return expenseBookStorage.getExpenseTrackerFilePath(); + } + + @Override + public Optional readExpenseTracker() throws DataConversionException, IOException { + return readExpenseTracker(expenseBookStorage.getExpenseTrackerFilePath()); + } + + @Override + public Optional readExpenseTracker(Path filePath) + throws DataConversionException, IOException { + logger.fine("Attempting to read data from file: " + filePath); + return expenseBookStorage.readExpenseTracker(filePath); + } + + @Override + public void saveExpenseTracker(ReadOnlyExpenseTracker expenseTracker) throws IOException { + saveExpenseTracker(expenseTracker, expenseBookStorage.getExpenseTrackerFilePath()); + } + + @Override + public void saveExpenseTracker(ReadOnlyExpenseTracker expenseTracker, Path filePath) throws IOException { + logger.fine("Attempting to write to data file: " + filePath); + expenseBookStorage.saveExpenseTracker(expenseTracker, filePath); + } +} diff --git a/src/main/java/seedu/address/storage/UserPrefsStorage.java b/src/main/java/fasttrack/storage/UserPrefsStorage.java similarity index 71% rename from src/main/java/seedu/address/storage/UserPrefsStorage.java rename to src/main/java/fasttrack/storage/UserPrefsStorage.java index 29eef178dbc..2f54a4fd8b2 100644 --- a/src/main/java/seedu/address/storage/UserPrefsStorage.java +++ b/src/main/java/fasttrack/storage/UserPrefsStorage.java @@ -1,15 +1,15 @@ -package seedu.address.storage; +package fasttrack.storage; import java.io.IOException; import java.nio.file.Path; import java.util.Optional; -import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.model.ReadOnlyUserPrefs; -import seedu.address.model.UserPrefs; +import fasttrack.commons.exceptions.DataConversionException; +import fasttrack.model.ReadOnlyUserPrefs; +import fasttrack.model.UserPrefs; /** - * Represents a storage for {@link seedu.address.model.UserPrefs}. + * Represents a storage for {@link fasttrack.model.UserPrefs}. */ public interface UserPrefsStorage { @@ -27,7 +27,7 @@ public interface UserPrefsStorage { Optional readUserPrefs() throws DataConversionException, IOException; /** - * Saves the given {@link seedu.address.model.ReadOnlyUserPrefs} to the storage. + * Saves the given {@link fasttrack.model.ReadOnlyUserPrefs} to the storage. * @param userPrefs cannot be null. * @throws IOException if there was any problem writing to the file. */ diff --git a/src/main/java/fasttrack/ui/CategoryCard.java b/src/main/java/fasttrack/ui/CategoryCard.java new file mode 100644 index 00000000000..a3c019f0075 --- /dev/null +++ b/src/main/java/fasttrack/ui/CategoryCard.java @@ -0,0 +1,57 @@ +package fasttrack.ui; + +import fasttrack.model.category.Category; +import fasttrack.model.util.UserInterfaceUtil; +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; + +/** + * A UI component that displays information of a {@code Category}. + */ +public class CategoryCard extends UiPart { + + private static final String FXML = "CategoryListCard.fxml"; + + + public final Category category; + + @FXML + private HBox cardPane; + @FXML + private Label id; + @FXML + private Label categoryName; + @FXML + private Label expenseCount; + + /** + * Creates a {@code CategoryCard} with the given {@code Category} and index to display. + */ + public CategoryCard(Category category, int displayedIndex, int associatedExpenseCount) { + super(FXML); + this.category = category; + id.setText(displayedIndex + ". "); + categoryName.setText(UserInterfaceUtil.capitalizeFirstLetter(category.getCategoryName())); + expenseCount.setText(String.valueOf(associatedExpenseCount)); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof CategoryCard)) { + return false; + } + + // state check + CategoryCard card = (CategoryCard) other; + return id.getText().equals(card.id.getText()) + && category.equals(card.category); + } +} diff --git a/src/main/java/fasttrack/ui/CategoryListPanel.java b/src/main/java/fasttrack/ui/CategoryListPanel.java new file mode 100644 index 00000000000..1bedc02fa46 --- /dev/null +++ b/src/main/java/fasttrack/ui/CategoryListPanel.java @@ -0,0 +1,54 @@ +package fasttrack.ui; + +import fasttrack.model.category.Category; +import fasttrack.model.expense.Expense; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.layout.Region; + +/** + * Panel containing the list of categories. + */ +public class CategoryListPanel extends UiPart { + private static final String FXML = "CategoryListPanel.fxml"; + private final ObservableList expenseObservableList; + + @FXML + private ListView categoryListView; + + /** + * Creates a {@code CategoryListPanel} with the given {@code ObservableList}. + */ + public CategoryListPanel(ObservableList categoryList, ObservableList expenseList) { + super(FXML); + this.expenseObservableList = expenseList; + categoryListView.setItems(categoryList); + categoryListView.setCellFactory(listView -> new CategoryListViewCell()); + } + + private int getAssociatedExpenseCount(Category category) { + return (int) expenseObservableList.stream() + .filter(e -> e.getCategory().equals(category)) + .count(); + } + + /** + * Custom {@code ListCell} that displays the graphics of a {@code Category} using a {@code CategoryCard}. + */ + class CategoryListViewCell extends ListCell { + @Override + protected void updateItem(Category category, boolean empty) { + super.updateItem(category, empty); + + if (empty || category == null) { + setGraphic(null); + setText(null); + } else { + setGraphic(new CategoryCard(category, getIndex() + 1, getAssociatedExpenseCount(category)).getRoot()); + } + } + } + +} diff --git a/src/main/java/fasttrack/ui/CommandBox.java b/src/main/java/fasttrack/ui/CommandBox.java new file mode 100644 index 00000000000..7aad050dbe2 --- /dev/null +++ b/src/main/java/fasttrack/ui/CommandBox.java @@ -0,0 +1,164 @@ +package fasttrack.ui; + +import fasttrack.logic.commands.CommandResult; +import fasttrack.logic.commands.exceptions.CommandException; +import fasttrack.logic.parser.exceptions.ParseException; +import javafx.beans.property.StringProperty; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.Node; +import javafx.scene.control.TextField; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.scene.layout.Region; +import javafx.stage.Window; + +/** + * The UI component that is responsible for receiving user command inputs. + */ +public class CommandBox extends UiPart { + + public static final String ERROR_STYLE_CLASS = "error"; + private static final String FXML = "CommandBox.fxml"; + + private final CommandExecutor commandExecutor; + + @FXML + private TextField commandTextField; + + /** + * Creates a {@code CommandBox} with the given {@code CommandExecutor}. + */ + public CommandBox(CommandExecutor commandExecutor, boolean initialiseAutocompletion) { + super(FXML); + this.commandExecutor = commandExecutor; + // calls #setStyleToDefault() whenever there is a change to the text of the command box. + commandTextField.textProperty().addListener((unused1, unused2, unused3) -> setStyleToDefault()); + if (initialiseAutocompletion) { + initialiseAutocompleteHandler(); + } + } + + + /** + * Handles the Enter button pressed event. + */ + @FXML + public void handleCommandEntered() { + String commandText = commandTextField.getText(); + if (commandText.equals("")) { + return; + } + + try { + commandExecutor.execute(commandText); + commandTextField.setText(""); + } catch (CommandException | ParseException e) { + setStyleToIndicateCommandFailure(); + } + } + + /** + * Sets the command box style to use the default style. + */ + private void setStyleToDefault() { + commandTextField.getStyleClass().remove(ERROR_STYLE_CLASS); + } + + /** + * Sets the command box style to indicate a failed command. + */ + private void setStyleToIndicateCommandFailure() { + ObservableList styleClass = commandTextField.getStyleClass(); + + if (styleClass.contains(ERROR_STYLE_CLASS)) { + return; + } + + styleClass.add(ERROR_STYLE_CLASS); + } + + /** + * Returns the current text property of the command text field + * @return the text property of the command text field + */ + public StringProperty getTextProperty() { + return commandTextField.textProperty(); + } + + /** + * Gives back focus to the command text field and positions the cursor at the end of the text. + */ + public void setFocus() { + commandTextField.requestFocus(); + commandTextField.positionCaret(commandTextField.getText().length()); + } + + /** + * Updates the command text field with the given category name by + * replacing the text after "c/" with the category name. + * @param categoryName The name of the category to be inserted into the command text field. + */ + public void updateCommandBoxText(String categoryName) { + String currentText = commandTextField.getText(); + int inputIndex = currentText.indexOf("c/") + 2; + String updatedString; + updatedString = currentText.substring(0, inputIndex) + categoryName; + commandTextField.setText(updatedString + " "); + } + + /** + * Adds a key press event filter to the command text field that listens for the UP arrow key. + * When the up arrow key is pressed, focus is given to the suggestion list if it is visible. + */ + private void initialiseAutocompleteHandler() { + commandTextField.addEventFilter(KeyEvent.KEY_PRESSED, event -> { + Window mainStage = commandTextField.getScene().getWindow(); + Node suggestionsList = mainStage.getScene().lookup("#suggestionListView"); + if (event.getCode() == KeyCode.UP && suggestionsList.isVisible()) { + suggestionsList.requestFocus(); + } + if (event.getCode() == KeyCode.ENTER && suggestionsList.isVisible()) { + suggestionsList.setVisible(false); + } + if (event.getCode() == KeyCode.TAB) { + // simulate UP key press + KeyEvent upEvent = new KeyEvent( + KeyEvent.KEY_PRESSED, + "", + "", + KeyCode.UP, + false, + false, + false, + false); + commandTextField.fireEvent(upEvent); + // simulate ENTER key press + KeyEvent enterEvent = new KeyEvent( + KeyEvent.KEY_PRESSED, + "", + "", KeyCode.ENTER, + false, + false, + false, + false); + suggestionsList.fireEvent(enterEvent); + event.consume(); + } + }); + } + + /** + * Represents a function that can execute commands. + */ + @FunctionalInterface + public interface CommandExecutor { + /** + * Executes the command and returns the result. + * + * @see fasttrack.logic.Logic#execute(String) + */ + CommandResult execute(String commandText) throws CommandException, ParseException; + } + +} diff --git a/src/main/java/fasttrack/ui/ExpenseCard.java b/src/main/java/fasttrack/ui/ExpenseCard.java new file mode 100644 index 00000000000..cc9da2ff5b7 --- /dev/null +++ b/src/main/java/fasttrack/ui/ExpenseCard.java @@ -0,0 +1,65 @@ +package fasttrack.ui; + +import fasttrack.model.expense.Expense; +import fasttrack.model.util.UserInterfaceUtil; +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; + +/** + * A UI component that displays information of a {@code Expense}. + */ +public class ExpenseCard extends UiPart { + + private static final String FXML = "ExpenseListCard.fxml"; + + + public final Expense expense; + + @FXML + private HBox cardPane; + @FXML + private Label id; + @FXML + private Label expenseName; + @FXML + private Label price; + @FXML + private Label category; + @FXML + private Label date; + + + /** + * Creates a {@code ExpenseCard} with the given {@code Expense} and index to display. + */ + public ExpenseCard(Expense expense, int displayedIndex) { + super(FXML); + this.expense = expense; + id.setText(displayedIndex + ". "); + expenseName.setText(UserInterfaceUtil.capitalizeFirstLetter(expense.getName())); + String categoryName = expense.getCategory().getCategoryName(); + category.setText(UserInterfaceUtil.capitalizeFirstLetter(categoryName)); + date.setText(UserInterfaceUtil.parseDate(expense.getDate())); + price.setText(UserInterfaceUtil.parsePrice(expense.getAmount())); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof ExpenseCard)) { + return false; + } + + // state check + ExpenseCard card = (ExpenseCard) other; + return id.getText().equals(card.id.getText()) + && expense.equals(card.expense); + } +} diff --git a/src/main/java/fasttrack/ui/ExpenseListPanel.java b/src/main/java/fasttrack/ui/ExpenseListPanel.java new file mode 100644 index 00000000000..6265ab34ec6 --- /dev/null +++ b/src/main/java/fasttrack/ui/ExpenseListPanel.java @@ -0,0 +1,56 @@ +package fasttrack.ui; + +import java.util.logging.Logger; + +import fasttrack.commons.core.LogsCenter; +import fasttrack.model.expense.Expense; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.layout.Region; + +/** + * Panel containing the list of expenses. + */ +public class ExpenseListPanel extends UiPart { + private static final String FXML = "ExpenseListPanel.fxml"; + private final Logger logger = LogsCenter.getLogger(ExpenseListPanel.class); + + @FXML + private ListView expenseListView; + + /** + * Creates a {@code ExpenseListPanel} with the given {@code ObservableList}. + */ + public ExpenseListPanel(ObservableList expenseList) { + super(FXML); + expenseListView.setItems(expenseList); + expenseListView.setCellFactory(listView -> new ExpenseListViewCell()); + } + + /** + * Refreshes the list of expenses and its related data. + */ + public void refreshList() { + expenseListView.refresh(); + } + + /** + * Custom {@code ListCell} that displays the graphics of a {@code Expense} using a {@code ExpenseCard}. + */ + class ExpenseListViewCell extends ListCell { + @Override + protected void updateItem(Expense expense, boolean empty) { + super.updateItem(expense, empty); + + if (empty || expense == null) { + setGraphic(null); + setText(null); + } else { + setGraphic(new ExpenseCard(expense, getIndex() + 1).getRoot()); + } + } + } + +} diff --git a/src/main/java/fasttrack/ui/HelpWindow.java b/src/main/java/fasttrack/ui/HelpWindow.java new file mode 100644 index 00000000000..f232e256f28 --- /dev/null +++ b/src/main/java/fasttrack/ui/HelpWindow.java @@ -0,0 +1,140 @@ +package fasttrack.ui; + +import java.util.logging.Logger; + +import fasttrack.commons.core.LogsCenter; +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.input.Clipboard; +import javafx.scene.input.ClipboardContent; +import javafx.stage.Stage; + +/** + * Controller for a help page + */ +public class HelpWindow extends UiPart { + + public static final String USERGUIDE_URL = + "https://ay2223s2-cs2103t-w09-2.github.io/tp/UserGuide.html"; + public static final String HELP_MESSAGE_COMMAND = "Features:\n" + + "help\n" + + " - Accesses this help guide\n\n" + + "addcat c/categoryName\n" + + " - Adds a new category to the expense tracker\n\n" + + "delcat index\n" + + " - Deletes a category from the expense tracker\n\n" + + "edcat index c/categoryName [s/summary]\n" + + " - Edits specified category in the expense tracker\n\n" + + "lcat\n" + + " - Shows all categories in the expense tracker\n\n" + + "add n/expenseName c/categoryName p/price [d/Date]\n" + + " - Adds an expense to the user's expense tracker\n\n" + + "delete index\n" + + " - Deletes expense at index [index]\n\n" + + "edexp index [c/categoryName] [n/expenseName] [d/Date] [p/price]\n" + + " - Edits specified expense in the expense tracker\n\n" + + "addrec c/categoryName n/itemName p/price t/timeframe sd/startDate [ed/endDate]\n" + + " - Adds a recurring expense to the user's expense tracker\n\n" + + "delrec index\n" + + " - Deletes a recurring expense at index [index]\n\n" + + "edrec index [c/categoryName] [n/itemName] [p/price] [t/timeframe] [ed/endDate]\n" + + " - Edits specified expense in the expense tracker\n\n" + + "lrec\n" + + " - Displays the list of recurring expenses\n\n" + + "set p/budget\n" + + " - Sets the monthly budget in the expense tracker\n\n" + + "scat index\n" + + " - Displays summary text of category\n\n" + + "find [keyword]\n" + + " - lists all expenses with matching keywords\n\n" + + "list [c/categoryName] [t/timeframe]\n" + + " - Displays the list of expenses with optional category or time-span filters\n\n" + + "CLEAR\n" + + " - Clears all the expenses and categories stored in FastTrack\n\n"; + + public static final String HELP_MESSAGE = "For more info, refer to the FastTrack user guide: " + + USERGUIDE_URL; + + private static final Logger logger = LogsCenter.getLogger(HelpWindow.class); + private static final String FXML = "HelpWindow.fxml"; + + @FXML + private Button copyButton; + + @FXML + private Label helpMessage; + + /** + * Creates a new HelpWindow. + * + * @param root Stage to use as the root of the HelpWindow. + */ + public HelpWindow(Stage root) { + super(FXML, root); + helpMessage.setText(HELP_MESSAGE); + } + + /** + * Creates a new HelpWindow. + */ + public HelpWindow() { + this(new Stage()); + } + + /** + * Shows the help window. + * @throws IllegalStateException + *
    + *
  • + * if this method is called on a thread other than the JavaFX Application Thread. + *
  • + *
  • + * if this method is called during animation or layout processing. + *
  • + *
  • + * if this method is called on the primary stage. + *
  • + *
  • + * if {@code dialogStage} is already showing. + *
  • + *
+ */ + public void show() { + logger.fine("Showing help page about the application."); + getRoot().show(); + getRoot().centerOnScreen(); + } + + /** + * Returns true if the help window is currently being shown. + */ + public boolean isShowing() { + return getRoot().isShowing(); + } + + /** + * Hides the help window. + */ + public void hide() { + getRoot().hide(); + } + + /** + * Focuses on the help window. + */ + public void focus() { + getRoot().requestFocus(); + } + + /** + * Copies the URL to the user guide to the clipboard. + */ + @FXML + private void copyUrl() { + final Clipboard clipboard = Clipboard.getSystemClipboard(); + final ClipboardContent url = new ClipboardContent(); + url.putString(USERGUIDE_URL); + clipboard.setContent(url); + } +} diff --git a/src/main/java/seedu/address/ui/MainWindow.java b/src/main/java/fasttrack/ui/MainWindow.java similarity index 65% rename from src/main/java/seedu/address/ui/MainWindow.java rename to src/main/java/fasttrack/ui/MainWindow.java index 9106c3aa6e5..b398df58fe8 100644 --- a/src/main/java/seedu/address/ui/MainWindow.java +++ b/src/main/java/fasttrack/ui/MainWindow.java @@ -1,21 +1,23 @@ -package seedu.address.ui; +package fasttrack.ui; import java.util.logging.Logger; +import fasttrack.commons.core.GuiSettings; +import fasttrack.commons.core.LogsCenter; +import fasttrack.logic.Logic; +import fasttrack.logic.commands.CommandResult; +import fasttrack.logic.commands.exceptions.CommandException; +import fasttrack.logic.parser.exceptions.ParseException; +import fasttrack.model.AnalyticModelManager; import javafx.event.ActionEvent; import javafx.fxml.FXML; +import javafx.scene.Parent; import javafx.scene.control.MenuItem; import javafx.scene.control.TextInputControl; import javafx.scene.input.KeyCombination; import javafx.scene.input.KeyEvent; import javafx.scene.layout.StackPane; import javafx.stage.Stage; -import seedu.address.commons.core.GuiSettings; -import seedu.address.commons.core.LogsCenter; -import seedu.address.logic.Logic; -import seedu.address.logic.commands.CommandResult; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.logic.parser.exceptions.ParseException; /** * The Main Window. Provides the basic application layout containing @@ -31,9 +33,12 @@ public class MainWindow extends UiPart { private Logic logic; // Independent Ui parts residing in this Ui container - private PersonListPanel personListPanel; + private ExpenseListPanel expenseListPanel; private ResultDisplay resultDisplay; private HelpWindow helpWindow; + private ResultsHeader resultsHeader; + private ResultsDetails resultsDetails; + private StatisticsPanel statisticsPanel; @FXML private StackPane commandBoxPlaceholder; @@ -42,7 +47,7 @@ public class MainWindow extends UiPart { private MenuItem helpMenuItem; @FXML - private StackPane personListPanelPlaceholder; + private StackPane listPanelPlaceholder; @FXML private StackPane resultDisplayPlaceholder; @@ -50,6 +55,16 @@ public class MainWindow extends UiPart { @FXML private StackPane statusbarPlaceholder; + @FXML + private StackPane resultsHeaderPlaceholder; + + @FXML + private StackPane resultsDetailsPlaceholder; + + @FXML + private StackPane statisticsPlaceholder; + + /** * Creates a {@code MainWindow} with the given {@code Stage} and {@code Logic}. */ @@ -110,17 +125,33 @@ private void setAccelerator(MenuItem menuItem, KeyCombination keyCombination) { * Fills up all the placeholders of this window. */ void fillInnerParts() { - personListPanel = new PersonListPanel(logic.getFilteredPersonList()); - personListPanelPlaceholder.getChildren().add(personListPanel.getRoot()); + expenseListPanel = new ExpenseListPanel(logic.getFilteredExpenseList()); + listPanelPlaceholder.getChildren().add(expenseListPanel.getRoot()); resultDisplay = new ResultDisplay(); resultDisplayPlaceholder.getChildren().add(resultDisplay.getRoot()); + resultsHeader = new ResultsHeader(logic.getAppliedCategoryFilter()); + resultsHeaderPlaceholder.getChildren().add(resultsHeader.getRoot()); + + resultsDetails = new ResultsDetails(logic.getFilteredExpenseList(), + logic.getRecurringExpenseManagerList(), + logic.getFilteredCategoryList(), logic.getAppliedTimeSpanFilter()); + resultsDetailsPlaceholder.getChildren().add(resultsDetails.getRoot()); + + statisticsPanel = new StatisticsPanel(new AnalyticModelManager(logic.getExpenseTracker())); + statisticsPlaceholder.getChildren().add(statisticsPanel.getRoot()); + StatusBarFooter statusBarFooter = new StatusBarFooter(logic.getAddressBookFilePath()); statusbarPlaceholder.getChildren().add(statusBarFooter.getRoot()); - CommandBox commandBox = new CommandBox(this::executeCommand); + CommandBox commandBox = new CommandBox(this::executeCommand, true); + SuggestionListPanel suggestionListPanel = new SuggestionListPanel(logic.getFilteredCategoryList(), commandBox); + + commandBoxPlaceholder.getChildren().add(commandBox.getRoot()); + resultDisplayPlaceholder.getChildren().add(suggestionListPanel.getRoot()); + } /** @@ -163,29 +194,49 @@ private void handleExit() { primaryStage.hide(); } - public PersonListPanel getPersonListPanel() { - return personListPanel; + /** + * Toggles the display between the different screens + * @param screenType the screen to be shown + */ + public void switchListPanel(ScreenType screenType) { + resultsHeader.setHeader(screenType); + resultsDetails.switchDetails(screenType); + Parent listPanelRoot; + switch (screenType) { + case EXPENSE_SCREEN: + listPanelRoot = new ExpenseListPanel(logic.getFilteredExpenseList()).getRoot(); + break; + case CATEGORY_SCREEN: + listPanelRoot = new CategoryListPanel(logic.getFilteredCategoryList(), + logic.getFilteredExpenseList()).getRoot(); + break; + case RECURRING_EXPENSE_SCREEN: + listPanelRoot = new RecurringExpensePanel(logic.getRecurringExpenseManagerList()).getRoot(); + break; + default: + throw new IllegalArgumentException("Screen type does not exist"); + } + listPanelPlaceholder.getChildren().setAll(listPanelRoot); } /** * Executes the command and returns the result. * - * @see seedu.address.logic.Logic#execute(String) + * @see fasttrack.logic.Logic#execute(String) */ private CommandResult executeCommand(String commandText) throws CommandException, ParseException { try { CommandResult commandResult = logic.execute(commandText); + expenseListPanel.refreshList(); logger.info("Result: " + commandResult.getFeedbackToUser()); resultDisplay.setFeedbackToUser(commandResult.getFeedbackToUser()); - + switchListPanel(commandResult.getScreenType()); if (commandResult.isShowHelp()) { handleHelp(); } - if (commandResult.isExit()) { handleExit(); } - return commandResult; } catch (CommandException | ParseException e) { logger.info("Invalid command: " + commandText); diff --git a/src/main/java/fasttrack/ui/RecurringExpenseCard.java b/src/main/java/fasttrack/ui/RecurringExpenseCard.java new file mode 100644 index 00000000000..9fa270689f9 --- /dev/null +++ b/src/main/java/fasttrack/ui/RecurringExpenseCard.java @@ -0,0 +1,64 @@ +package fasttrack.ui; + +import fasttrack.model.expense.RecurringExpenseManager; +import fasttrack.model.util.UserInterfaceUtil; +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; + +/** + * A UI component that displays information of a + * {@code RecurringExpenseManager}. + */ +public class RecurringExpenseCard extends UiPart { + + private static final String FXML = "RecurringExpenseListCard.fxml"; + + public final RecurringExpenseManager recurringExpenseManager; + + @FXML + private HBox cardPane; + @FXML + private Label id; + @FXML + private Label expenseName; + @FXML + private Label price; + @FXML + private Label category; + @FXML + private Label frequency; + + /** + * Creates a {@code RecurringExpenseCard} with the given + * {@code RecurringExpenseManager} and index to display. + */ + public RecurringExpenseCard(RecurringExpenseManager recurringExpenseManager, int displayedIndex) { + super(FXML); + this.recurringExpenseManager = recurringExpenseManager; + id.setText(displayedIndex + ". "); + expenseName.setText(UserInterfaceUtil.capitalizeFirstLetter(recurringExpenseManager.getExpenseName())); + String categoryName = recurringExpenseManager.getExpenseCategory().getCategoryName(); + category.setText(UserInterfaceUtil.capitalizeFirstLetter(categoryName)); + price.setText(UserInterfaceUtil.parsePrice(recurringExpenseManager.getAmount())); + String recurringExpenseFrequency = recurringExpenseManager.getRecurringExpenseType().name(); + frequency.setText(UserInterfaceUtil.capitalizeFirstLetter(recurringExpenseFrequency.toLowerCase())); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + // instanceof handles nulls + if (!(other instanceof RecurringExpenseCard)) { + return false; + } + // state check + RecurringExpenseCard card = (RecurringExpenseCard) other; + return id.getText().equals(card.id.getText()) + && recurringExpenseManager.equals(card.recurringExpenseManager); + } +} diff --git a/src/main/java/fasttrack/ui/RecurringExpensePanel.java b/src/main/java/fasttrack/ui/RecurringExpensePanel.java new file mode 100644 index 00000000000..0853a3c8c8a --- /dev/null +++ b/src/main/java/fasttrack/ui/RecurringExpensePanel.java @@ -0,0 +1,54 @@ +package fasttrack.ui; + +import fasttrack.model.expense.RecurringExpenseManager; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.layout.Region; + + +/** + * Panel containing the list of recurring expenses. + */ +public class RecurringExpensePanel extends UiPart { + private static final String FXML = "RecurringExpenseListPanel.fxml"; + + @FXML + private ListView recurringExpenseListView; + + /** + * Creates a {@code RecurringExpensePanel} with the given {@code ObservableList}. + */ + public RecurringExpensePanel(ObservableList recurringExpenseList) { + super(FXML); + recurringExpenseListView.setItems(recurringExpenseList); + recurringExpenseListView.setCellFactory(listView -> new RecurringExpenseListViewCell()); + } + + /** + * Refreshes the list of recurring expenses and its related data. + */ + public void refreshList() { + recurringExpenseListView.refresh(); + } + + /** + * Custom {@code ListCell} that displays the graphics of a {@code RecurringExpenseManager} + * using a {@code RecurringExpenseCard}. + */ + class RecurringExpenseListViewCell extends ListCell { + @Override + protected void updateItem(RecurringExpenseManager expense, boolean empty) { + super.updateItem(expense, empty); + + if (empty || expense == null) { + setGraphic(null); + setText(null); + } else { + setGraphic(new RecurringExpenseCard(expense, getIndex() + 1).getRoot()); + } + } + } + +} diff --git a/src/main/java/seedu/address/ui/ResultDisplay.java b/src/main/java/fasttrack/ui/ResultDisplay.java similarity index 95% rename from src/main/java/seedu/address/ui/ResultDisplay.java rename to src/main/java/fasttrack/ui/ResultDisplay.java index 7d98e84eedf..a670b4af1d6 100644 --- a/src/main/java/seedu/address/ui/ResultDisplay.java +++ b/src/main/java/fasttrack/ui/ResultDisplay.java @@ -1,4 +1,4 @@ -package seedu.address.ui; +package fasttrack.ui; import static java.util.Objects.requireNonNull; diff --git a/src/main/java/fasttrack/ui/ResultsDetails.java b/src/main/java/fasttrack/ui/ResultsDetails.java new file mode 100644 index 00000000000..b9f6e7a5518 --- /dev/null +++ b/src/main/java/fasttrack/ui/ResultsDetails.java @@ -0,0 +1,131 @@ +package fasttrack.ui; + +import fasttrack.logic.parser.ParserUtil; +import fasttrack.model.category.Category; +import fasttrack.model.expense.Expense; +import fasttrack.model.expense.RecurringExpenseManager; +import javafx.beans.binding.Bindings; +import javafx.beans.binding.IntegerBinding; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.SimpleIntegerProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.layout.Region; +import javafx.util.StringConverter; + +/** + * A UI component that displays information of a {@code Expense} or {@code Category} + * such as the number of results and the time range filter applied. + */ +public class ResultsDetails extends UiPart { + + private static final String FXML = "ResultsDetails.fxml"; + private final IntegerProperty count; + private final ObservableList expenseList; + private final ObservableList categoryList; + private final ObservableList recurringExpenseManagersList; + + @FXML + private Label resultsCount; + @FXML + private Label dateFilter; + @FXML + private Label dateLabel; + + /** + * Creates a new ResultsDetails pane to display the details of each data + * @param expenseList the list of expenses to display + * @param recurringExpenseManagersList the list of recurring expense managers to display + * @param categoryList the list of categories to display + * @param timeFilter the filter to apply to expense based on month, week, year + */ + public ResultsDetails(ObservableList expenseList, + ObservableList recurringExpenseManagersList, + ObservableList categoryList, + SimpleObjectProperty timeFilter) { + super(FXML); + this.count = new SimpleIntegerProperty(); + this.expenseList = expenseList; + this.categoryList = categoryList; + this.recurringExpenseManagersList = recurringExpenseManagersList; + dateLabel.setText("Date:"); + bindResultsCount(ScreenType.EXPENSE_SCREEN); + dateFilter.textProperty().bindBidirectional(timeFilter, new CustomStringConverter()); + } + + + /** + * Switches the details displayed on the GUI based on an enum indicating + * which screen is being displayed. + * @param screenType an enum indicating which screen is being displayed. + */ + public void switchDetails(ScreenType screenType) { + bindResultsCount(screenType); + if (screenType == ScreenType.EXPENSE_SCREEN) { + dateLabel.setText("Time:"); + } else if (screenType == ScreenType.CATEGORY_SCREEN || screenType == ScreenType.RECURRING_EXPENSE_SCREEN) { + dateLabel.setText(""); + dateFilter.setText(""); + } + } + + /** + * Helper method used by the switchDetails method to bind the count of the number of items + * in the expenseList, categoryList or recurringExpenseManagerList to the count variable, + * which is then displayed in the GUI. + * @param screenType an enum indicating which screen is being displayed. + */ + private void bindResultsCount(ScreenType screenType) { + if (screenType == ScreenType.EXPENSE_SCREEN) { + IntegerBinding expenseListSizeBinding = Bindings.size(expenseList); + count.bind(expenseListSizeBinding); + } else if (screenType == ScreenType.CATEGORY_SCREEN) { + IntegerBinding categoryListSizeBinding = Bindings.size(categoryList); + count.bind(categoryListSizeBinding); + } else if (screenType == ScreenType.RECURRING_EXPENSE_SCREEN) { + IntegerBinding recurringExpenseListSizeBinding = Bindings.size(recurringExpenseManagersList); + count.bind(recurringExpenseListSizeBinding); + } + resultsCount.textProperty().bind(count.asString()); + } + + + /** + * A custom string converter for the date filter. + */ + private static class CustomStringConverter extends StringConverter { + @Override + public String toString(ParserUtil.Timespan myEnum) { + return myEnum.toString(); + } + /** + * Not implemented for this class, always returns null. + * @param string the string to convert + * @return null + */ + @Override + public ParserUtil.Timespan fromString(String string) { + return null; + } + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof ResultsDetails)) { + return false; + } + + // state check + ResultsDetails details = (ResultsDetails) other; + return resultsCount.getText().equals(details.resultsCount.getText()) + && dateFilter.getText().equals(details.dateFilter.getText()); + } +} diff --git a/src/main/java/fasttrack/ui/ResultsHeader.java b/src/main/java/fasttrack/ui/ResultsHeader.java new file mode 100644 index 00000000000..1c788eb5a50 --- /dev/null +++ b/src/main/java/fasttrack/ui/ResultsHeader.java @@ -0,0 +1,91 @@ +package fasttrack.ui; + +import fasttrack.model.category.Category; +import fasttrack.model.util.UserInterfaceUtil; +import javafx.beans.property.SimpleObjectProperty; +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.layout.Region; +import javafx.util.StringConverter; + +/** + * A UI component that displays information of a {@code Expense} or {@code Category} + * with a title and the category filter applied if any. + */ +public class ResultsHeader extends UiPart { + + private static final String FXML = "ResultsHeader.fxml"; + + @FXML + private Label resultType; + @FXML + private Label filterType; + + /** + * Creates a {@code ResultsHeader} with the given {@code categoryFilter}. + */ + public ResultsHeader(SimpleObjectProperty categoryFilter) { + super(FXML); + filterType.textProperty().bindBidirectional(categoryFilter, new CategoryStringConverter()); + resultType.setText("Expenses"); + } + + /** + * Sets the header of the results pane based on the given screen type. + * @param screenType the type of screen to set the header for + */ + public void setHeader(ScreenType screenType) { + if (screenType == ScreenType.EXPENSE_SCREEN) { + resultType.setText("Expenses"); + return; + } else if (screenType == ScreenType.CATEGORY_SCREEN) { + resultType.setText("Category"); + } else if (screenType == ScreenType.RECURRING_EXPENSE_SCREEN) { + resultType.setText("Recurring Expenses"); + } + filterType.setText(""); + } + + /** + * A custom string converter that converts a Category object to its capitalized category name, + * or "All" if the category is null. + * This is used to display the currently selected category filter, which can be null if no + * category filter is selected + */ + public static class CategoryStringConverter extends StringConverter { + @Override + public String toString(Category category) { + if (category != null) { + return UserInterfaceUtil.capitalizeFirstLetter(category.getCategoryName()); + } + return "All"; + } + /** + * Not implemented for this class, always returns null. + * @param string the string to convert + * @return null + */ + @Override + public Category fromString(String string) { + return null; + } + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof ResultsHeader)) { + return false; + } + + // state check + ResultsHeader header = (ResultsHeader) other; + return resultType.getText().equals(header.resultType.getText()) + && filterType.getText().equals(header.filterType.getText()); + } +} diff --git a/src/main/java/fasttrack/ui/ScreenType.java b/src/main/java/fasttrack/ui/ScreenType.java new file mode 100644 index 00000000000..9f03639634f --- /dev/null +++ b/src/main/java/fasttrack/ui/ScreenType.java @@ -0,0 +1,12 @@ +package fasttrack.ui; + +/** + * Represents different types of analytics that can be calculated in FastTrack + * MONTHLY_SPENT: Represents the total amount spent in a month. + */ + +public enum ScreenType { + EXPENSE_SCREEN, + CATEGORY_SCREEN, + RECURRING_EXPENSE_SCREEN +} diff --git a/src/main/java/fasttrack/ui/StatisticsPanel.java b/src/main/java/fasttrack/ui/StatisticsPanel.java new file mode 100644 index 00000000000..faeacdca14a --- /dev/null +++ b/src/main/java/fasttrack/ui/StatisticsPanel.java @@ -0,0 +1,184 @@ +package fasttrack.ui; + +import fasttrack.model.AnalyticModel; +import fasttrack.model.util.AnalyticsType; +import javafx.beans.property.DoubleProperty; +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; + +/** + * The UI component that is responsible for displaying user spending statistics + */ +public class StatisticsPanel extends UiPart { + + private static final String FXML = "StatisticsPanel.fxml"; + private final AnalyticModel analyticModel; + + @FXML + private Label monthlySpending; + @FXML + private Label weeklySpending; + @FXML + private Label monthlyRemaining; + @FXML + private Label weeklyRemaining; + @FXML + private Label monthlyChange; + @FXML + private Label weeklyChange; + @FXML + private Label totalSpending; + @FXML + private Label budgetPercentage; + @FXML + private Label monthlySign; + @FXML + private Label weeklySign; + @FXML + private Label budgetAdvice; + @FXML + private HBox weeklyChangeBackground; + @FXML + private HBox monthlyChangeBackground; + + /** + * Creates a new StatisticsPanel object with the specified AnalyticModel object + * and binds all values to the statistics + * @param analyticModel the AnalyticModel object for data retrieval + */ + public StatisticsPanel(AnalyticModel analyticModel) { + super(FXML); + this.analyticModel = analyticModel; + bindAllValuesToStatistics(); + updateBudgetAdvice(analyticModel.getMonthlySpent().doubleValue()); + analyticModel.getMonthlySpent().addListener((observable, oldValue, newValue) -> { + updateBudgetAdvice(newValue); + }); + analyticModel.getMonthlyBudgetProperty().addListener((observable, oldValue, newValue) -> { + updateBudgetAdvice(analyticModel.getMonthlySpent().get()); + }); + } + + /** + * Binds the given AnalyticsType value to the given Label object + * and formats the label text as a price or percentage + * @param analyticsType the AnalyticsType value to bind + * @param labelToBind the Label object to bind the value to + * @param isPrice true if the value should be formatted as a price, false if to be formatted as a percentage + */ + private void bindValueToStatistic(AnalyticsType analyticsType, Label labelToBind, boolean isPrice) { + String formatString = isPrice ? "$%.2f" : "%.2f%%"; + labelToBind.textProperty().bind(analyticModel.getAnalyticsData(analyticsType).asString(formatString)); + } + + /** + * Binds the given AnalyticsType value to the given Label object + * and updates the percentage change indicator style based on the value + * @param analyticsType the AnalyticsType value to bind + */ + private void bindValueToChangeIndicator(AnalyticsType analyticsType) { + DoubleProperty changeValue = analyticModel.getAnalyticsData(analyticsType); + if (analyticsType != AnalyticsType.WEEKLY_CHANGE + && analyticsType != AnalyticsType.MONTHLY_CHANGE) { + throw new IllegalArgumentException( + "The change indicator only accepts MONTHLY_CHANGE or WEEKLY_CHANGE!"); + } + // Updating weekly change indicator + if (analyticsType == AnalyticsType.WEEKLY_CHANGE) { + weeklyChange.textProperty().bind(changeValue.asString("%.2f%%")); + updateChangeIndicatorStyles( + weeklyChange, weeklySign, weeklyChangeBackground, changeValue.doubleValue()); + changeValue.addListener((observable, oldValue, newValue) -> { + updateChangeIndicatorStyles( + weeklyChange, weeklySign, weeklyChangeBackground, newValue.doubleValue()); + }); + } else { + // Updating monthly change indicator + monthlyChange.textProperty().bind(changeValue.asString("%.2f%%")); + updateChangeIndicatorStyles( + monthlyChange, monthlySign, monthlyChangeBackground, changeValue.doubleValue()); + changeValue.addListener((observable, oldValue, newValue) -> { + updateChangeIndicatorStyles( + monthlyChange, monthlySign, monthlyChangeBackground, newValue.doubleValue()); + }); + } + } + + private void updateChangeIndicatorStyles(Label labelToUpdate, + Label signToUpdate, + HBox backgroundToUpdate, double value) { + // Determine the CSS classes to apply based on the value of the change + String textColorClass = (value > 0) + ? "negative_change_indicator" + : "positive_change_indicator"; + String backgroundColorClass = (value > 0) + ? "change_indicator_background_negative" + : "change_indicator_background_positive"; + // Add sign label with a plus if the value is positive + signToUpdate.setText((value >= 0) ? "+" : ""); + // Update the style classes with new background + signToUpdate.getStyleClass().removeAll("negative_change_indicator", + "positive_change_indicator"); + signToUpdate.getStyleClass().add(textColorClass); + backgroundToUpdate.getStyleClass() + .removeAll("change_indicator_background_positive", + "change_indicator_background_negative"); + backgroundToUpdate.getStyleClass().add(backgroundColorClass); + // Update the main labels showing the values + labelToUpdate.getStyleClass() + .removeAll("negative_change_indicator", + "positive_change_indicator"); + labelToUpdate.getStyleClass().add(textColorClass); + } + + private void bindAllValuesToStatistics() { + bindValueToStatistic(AnalyticsType.MONTHLY_SPENT, monthlySpending, true); + bindValueToStatistic(AnalyticsType.MONTHLY_REMAINING, monthlyRemaining, true); + bindValueToStatistic(AnalyticsType.WEEKLY_SPENT, weeklySpending, true); + bindValueToStatistic(AnalyticsType.WEEKLY_REMAINING, weeklyRemaining, true); + bindValueToStatistic(AnalyticsType.TOTAL_SPENT, totalSpending, true); + bindValueToStatistic(AnalyticsType.BUDGET_PERCENTAGE, budgetPercentage, false); + bindValueToChangeIndicator(AnalyticsType.WEEKLY_CHANGE); + bindValueToChangeIndicator(AnalyticsType.MONTHLY_CHANGE); + } + + /** + * Updates the budget advice text based on the new value of the monthly spent. + * @param newValue the new value of the monthly spent + */ + private void updateBudgetAdvice(Number newValue) { + String adviceText = "Great job! You are within your budget!"; + if (analyticModel.getBudget() != 0 && newValue.doubleValue() > analyticModel.getBudget()) { + adviceText = "You have exceeded your monthly budget!"; + } + budgetAdvice.setText(adviceText); + } + + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof StatisticsPanel)) { + return false; + } + + // state check + StatisticsPanel statistics = (StatisticsPanel) other; + return monthlySpending.getText().equals(statistics.monthlySpending.getText()) + && monthlyRemaining.getText().equals(statistics.monthlyRemaining.getText()) + && weeklySpending.getText().equals(statistics.weeklySpending.getText()) + && weeklyRemaining.getText().equals(statistics.weeklyRemaining.getText()) + && weeklyChange.getText().equals(statistics.weeklyChange.getText()) + && monthlyChange.getText().equals(statistics.monthlyChange.getText()) + && totalSpending.getText().equals(statistics.totalSpending.getText()) + && budgetPercentage.getText().equals(statistics.budgetPercentage.getText()); + } + +} diff --git a/src/main/java/seedu/address/ui/StatusBarFooter.java b/src/main/java/fasttrack/ui/StatusBarFooter.java similarity index 96% rename from src/main/java/seedu/address/ui/StatusBarFooter.java rename to src/main/java/fasttrack/ui/StatusBarFooter.java index b577f829423..8a73e2edd31 100644 --- a/src/main/java/seedu/address/ui/StatusBarFooter.java +++ b/src/main/java/fasttrack/ui/StatusBarFooter.java @@ -1,4 +1,4 @@ -package seedu.address.ui; +package fasttrack.ui; import java.nio.file.Path; import java.nio.file.Paths; diff --git a/src/main/java/fasttrack/ui/SuggestionCard.java b/src/main/java/fasttrack/ui/SuggestionCard.java new file mode 100644 index 00000000000..7a48badf31c --- /dev/null +++ b/src/main/java/fasttrack/ui/SuggestionCard.java @@ -0,0 +1,49 @@ +package fasttrack.ui; + +import fasttrack.model.category.Category; +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; + +/** + * A UI component that displays information of a {@code Category}. + */ +public class SuggestionCard extends UiPart { + + private static final String FXML = "SuggestionListCard.fxml"; + + + public final Category category; + + @FXML + private HBox cardPane; + @FXML + private Label categoryName; + + /** + * Creates a {@code CategoryCard} with the given {@code Category} and index to display. + */ + public SuggestionCard(Category category) { + super(FXML); + this.category = category; + categoryName.setText(category.getCategoryName()); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof SuggestionCard)) { + return false; + } + + // state check + SuggestionCard card = (SuggestionCard) other; + return category.equals(card.category); + } +} diff --git a/src/main/java/fasttrack/ui/SuggestionListPanel.java b/src/main/java/fasttrack/ui/SuggestionListPanel.java new file mode 100644 index 00000000000..4b4517f20e9 --- /dev/null +++ b/src/main/java/fasttrack/ui/SuggestionListPanel.java @@ -0,0 +1,177 @@ +package fasttrack.ui; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import fasttrack.model.category.Category; +import javafx.collections.ObservableList; +import javafx.collections.transformation.FilteredList; +import javafx.fxml.FXML; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.scene.layout.Region; + +/** + * Panel containing the list of expenses. + */ +public class SuggestionListPanel extends UiPart { + private static final String FXML = "SuggestionListPanel.fxml"; + + @FXML + private ListView suggestionListView; + + private final FilteredList filteredCategoryList; + private final CommandBox commandBox; + + + /** + * Creates a {@code SuggestionListPanel} with the given {@code ObservableList}. + * @param categoryList the list of categories to use for the suggestions + * @param commandBox the CommandBox to use for autocomplete + */ + public SuggestionListPanel(ObservableList categoryList, CommandBox commandBox) { + super(FXML); + suggestionListView.setCellFactory(listView -> new SuggestionListViewCell()); + this.filteredCategoryList = new FilteredList<>(categoryList); + suggestionListView.setItems(filteredCategoryList); + this.commandBox = commandBox; + this.suggestionListView.setVisible(false); + initialiseAutocompleteHandlers(); + } + + /** + * Convenience method to initialise the autocomplete handlers for the suggestion list. + * Selects the last item in the suggestion list when it gains focus + * Adds a key press listener to the suggestion list + * Loads the autocomplete filter to show suggestions based on user input + */ + private void initialiseAutocompleteHandlers() { + selectLastItemOnFocus(); + addKeyPressListener(); + loadAutocompleteFilter(); + } + + /** + * Sets the focus on the last item in the suggestion list when the suggestion list gains focus. + */ + private void selectLastItemOnFocus() { + suggestionListView.focusedProperty().addListener((observable, oldFocus, currentFocus) -> { + if (currentFocus && suggestionListView.isVisible()) { + suggestionListView.getSelectionModel().select(filteredCategoryList.size() - 1); + } + }); + } + + /** + * Adds a key press listener to the suggestion list to handle ENTER and DOWN arrow key events. + * On ENTER, the suggested text should be set to the command box + * with focus returned to the command box. + * On DOWN, check if the user is going to navigate out of the suggestion list and + * return focus to the command box. + */ + private void addKeyPressListener() { + suggestionListView.addEventFilter(KeyEvent.KEY_PRESSED, event -> { + + if (KeyCode.ENTER == event.getCode() || KeyCode.TAB == event.getCode()) { + updateSuggestedText(); + event.consume(); + } + if (KeyCode.DOWN == event.getCode()) { + handleDownArrowKey(event); + } + }); + } + + /** + * Sets the text in the CommandBox to the selected category in the suggestion list + * and hides the suggestion list. + */ + private void updateSuggestedText() { + Category selectedCategory = suggestionListView.getSelectionModel().getSelectedItem(); + commandBox.updateCommandBoxText(selectedCategory.getCategoryName()); + suggestionListView.setVisible(false); + commandBox.setFocus(); + } + + + /** + * Handles the DOWN arrow key event in the suggestion list. + * Checks if the user is going to navigate out of the suggestion list and + * return focus to the command box. + * @param event the key event to handle + */ + private void handleDownArrowKey(KeyEvent event) { + int selectedIndex = suggestionListView.getSelectionModel().getSelectedIndex(); + if (selectedIndex == filteredCategoryList.size() - 1) { + suggestionListView.getSelectionModel().clearSelection(); + commandBox.setFocus(); + event.consume(); + } + } + + /** + * Loads the autocomplete filter for the CommandBox. + */ + private void loadAutocompleteFilter() { + commandBox.getTextProperty().addListener((observable, oldValue, newValue) -> { + // Check if the user is currently typing a category e.g. "c/food" + boolean isTypingCategory = newValue.matches(".*c/[^\\s]*$"); + if (newValue.contains("c/") && isTypingCategory) { + getAutocompleteSuggestions(newValue); + if (!filteredCategoryList.isEmpty()) { + suggestionListView.setVisible(true); + return; + } + } + // Hide the suggestion list if it is empty or the user is not typing a category + suggestionListView.getSelectionModel().clearSelection(); + suggestionListView.setVisible(false); + }); + } + + /** + * Gets the autocomplete suggestions for the input string. + * @param input the input string to get autocomplete suggestions for + */ + public void getAutocompleteSuggestions(String input) { + String pattern = "c/(\\S+)"; + Pattern exp = Pattern.compile(pattern); + Matcher matcher = exp.matcher(input); + if (matcher.find()) { + String categoryInput = matcher.group(1); + filteredCategoryList.setPredicate(category -> inputMatchesCategoryName(category, categoryInput)); + return; + } + filteredCategoryList.setPredicate(category -> true); + suggestionListView.scrollTo(filteredCategoryList.size() - 1); + } + + /** + * Checks if the input matches the category name. + * @param category the category to check + * @param input the input to check against the category name + * @return true if the input matches the category name, false otherwise + */ + private boolean inputMatchesCategoryName(Category category, String input) { + return category.getCategoryName().toLowerCase().startsWith(input.toLowerCase()); + } + + /** + * Custom {@code ListCell} that displays the graphics of a {@code Category} using a {@code SuggestionCard}. + */ + class SuggestionListViewCell extends ListCell { + @Override + protected void updateItem(Category category, boolean empty) { + super.updateItem(category, empty); + if (empty || category == null) { + setGraphic(null); + setText(null); + } else { + setGraphic(new SuggestionCard(category).getRoot()); + } + } + } + +} diff --git a/src/main/java/seedu/address/ui/Ui.java b/src/main/java/fasttrack/ui/Ui.java similarity index 86% rename from src/main/java/seedu/address/ui/Ui.java rename to src/main/java/fasttrack/ui/Ui.java index 17aa0b494fe..adf2aa011aa 100644 --- a/src/main/java/seedu/address/ui/Ui.java +++ b/src/main/java/fasttrack/ui/Ui.java @@ -1,4 +1,4 @@ -package seedu.address.ui; +package fasttrack.ui; import javafx.stage.Stage; diff --git a/src/main/java/seedu/address/ui/UiManager.java b/src/main/java/fasttrack/ui/UiManager.java similarity index 87% rename from src/main/java/seedu/address/ui/UiManager.java rename to src/main/java/fasttrack/ui/UiManager.java index fdf024138bc..ac4b7e32132 100644 --- a/src/main/java/seedu/address/ui/UiManager.java +++ b/src/main/java/fasttrack/ui/UiManager.java @@ -1,16 +1,16 @@ -package seedu.address.ui; +package fasttrack.ui; import java.util.logging.Logger; +import fasttrack.MainApp; +import fasttrack.commons.core.LogsCenter; +import fasttrack.commons.util.StringUtil; +import fasttrack.logic.Logic; import javafx.application.Platform; import javafx.scene.control.Alert; import javafx.scene.control.Alert.AlertType; import javafx.scene.image.Image; import javafx.stage.Stage; -import seedu.address.MainApp; -import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.util.StringUtil; -import seedu.address.logic.Logic; /** * The manager of the UI component. @@ -20,7 +20,7 @@ public class UiManager implements Ui { public static final String ALERT_DIALOG_PANE_FIELD_ID = "alertDialogPane"; private static final Logger logger = LogsCenter.getLogger(UiManager.class); - private static final String ICON_APPLICATION = "/images/address_book_32.png"; + private static final String ICON_APPLICATION = "/images/fasttrack_logo.png"; private Logic logic; private MainWindow mainWindow; @@ -35,7 +35,6 @@ public UiManager(Logic logic) { @Override public void start(Stage primaryStage) { logger.info("Starting UI..."); - //Set the application icon. primaryStage.getIcons().add(getImage(ICON_APPLICATION)); @@ -43,6 +42,10 @@ public void start(Stage primaryStage) { mainWindow = new MainWindow(primaryStage, logic); mainWindow.show(); //This should be called before creating other UI parts mainWindow.fillInnerParts(); + primaryStage.sizeToScene(); + primaryStage.setHeight(700); + primaryStage.setMinWidth(1000); + primaryStage.setMinHeight(700); } catch (Throwable e) { logger.severe(StringUtil.getDetails(e)); diff --git a/src/main/java/seedu/address/ui/UiPart.java b/src/main/java/fasttrack/ui/UiPart.java similarity index 97% rename from src/main/java/seedu/address/ui/UiPart.java rename to src/main/java/fasttrack/ui/UiPart.java index fc820e01a9c..fbc27aeced4 100644 --- a/src/main/java/seedu/address/ui/UiPart.java +++ b/src/main/java/fasttrack/ui/UiPart.java @@ -1,12 +1,12 @@ -package seedu.address.ui; +package fasttrack.ui; import static java.util.Objects.requireNonNull; import java.io.IOException; import java.net.URL; +import fasttrack.MainApp; import javafx.fxml.FXMLLoader; -import seedu.address.MainApp; /** * Represents a distinct part of the UI. e.g. Windows, dialogs, panels, status bars, etc. diff --git a/src/main/java/seedu/address/commons/core/Messages.java b/src/main/java/seedu/address/commons/core/Messages.java deleted file mode 100644 index 1deb3a1e469..00000000000 --- a/src/main/java/seedu/address/commons/core/Messages.java +++ /dev/null @@ -1,13 +0,0 @@ -package seedu.address.commons.core; - -/** - * Container for user visible messages. - */ -public class Messages { - - public static final String MESSAGE_UNKNOWN_COMMAND = "Unknown command"; - public static final String MESSAGE_INVALID_COMMAND_FORMAT = "Invalid command format! \n%1$s"; - public static final String MESSAGE_INVALID_PERSON_DISPLAYED_INDEX = "The person index provided is invalid"; - public static final String MESSAGE_PERSONS_LISTED_OVERVIEW = "%1$d persons listed!"; - -} diff --git a/src/main/java/seedu/address/logic/Logic.java b/src/main/java/seedu/address/logic/Logic.java deleted file mode 100644 index 92cd8fa605a..00000000000 --- a/src/main/java/seedu/address/logic/Logic.java +++ /dev/null @@ -1,50 +0,0 @@ -package seedu.address.logic; - -import java.nio.file.Path; - -import javafx.collections.ObservableList; -import seedu.address.commons.core.GuiSettings; -import seedu.address.logic.commands.CommandResult; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.person.Person; - -/** - * API of the Logic component - */ -public interface Logic { - /** - * Executes the command and returns the result. - * @param commandText The command as entered by the user. - * @return the result of the command execution. - * @throws CommandException If an error occurs during command execution. - * @throws ParseException If an error occurs during parsing. - */ - CommandResult execute(String commandText) throws CommandException, ParseException; - - /** - * Returns the AddressBook. - * - * @see seedu.address.model.Model#getAddressBook() - */ - ReadOnlyAddressBook getAddressBook(); - - /** Returns an unmodifiable view of the filtered list of persons */ - ObservableList getFilteredPersonList(); - - /** - * Returns the user prefs' address book file path. - */ - Path getAddressBookFilePath(); - - /** - * Returns the user prefs' GUI settings. - */ - GuiSettings getGuiSettings(); - - /** - * Set the user prefs' GUI settings. - */ - void setGuiSettings(GuiSettings guiSettings); -} diff --git a/src/main/java/seedu/address/logic/LogicManager.java b/src/main/java/seedu/address/logic/LogicManager.java deleted file mode 100644 index 9d9c6d15bdc..00000000000 --- a/src/main/java/seedu/address/logic/LogicManager.java +++ /dev/null @@ -1,81 +0,0 @@ -package seedu.address.logic; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.logging.Logger; - -import javafx.collections.ObservableList; -import seedu.address.commons.core.GuiSettings; -import seedu.address.commons.core.LogsCenter; -import seedu.address.logic.commands.Command; -import seedu.address.logic.commands.CommandResult; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.logic.parser.AddressBookParser; -import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.Model; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.person.Person; -import seedu.address.storage.Storage; - -/** - * The main LogicManager of the app. - */ -public class LogicManager implements Logic { - public static final String FILE_OPS_ERROR_MESSAGE = "Could not save data to file: "; - private final Logger logger = LogsCenter.getLogger(LogicManager.class); - - private final Model model; - private final Storage storage; - private final AddressBookParser addressBookParser; - - /** - * Constructs a {@code LogicManager} with the given {@code Model} and {@code Storage}. - */ - public LogicManager(Model model, Storage storage) { - this.model = model; - this.storage = storage; - addressBookParser = new AddressBookParser(); - } - - @Override - public CommandResult execute(String commandText) throws CommandException, ParseException { - logger.info("----------------[USER COMMAND][" + commandText + "]"); - - CommandResult commandResult; - Command command = addressBookParser.parseCommand(commandText); - commandResult = command.execute(model); - - try { - storage.saveAddressBook(model.getAddressBook()); - } catch (IOException ioe) { - throw new CommandException(FILE_OPS_ERROR_MESSAGE + ioe, ioe); - } - - return commandResult; - } - - @Override - public ReadOnlyAddressBook getAddressBook() { - return model.getAddressBook(); - } - - @Override - public ObservableList getFilteredPersonList() { - return model.getFilteredPersonList(); - } - - @Override - public Path getAddressBookFilePath() { - return model.getAddressBookFilePath(); - } - - @Override - public GuiSettings getGuiSettings() { - return model.getGuiSettings(); - } - - @Override - public void setGuiSettings(GuiSettings guiSettings) { - model.setGuiSettings(guiSettings); - } -} diff --git a/src/main/java/seedu/address/logic/commands/AddCommand.java b/src/main/java/seedu/address/logic/commands/AddCommand.java deleted file mode 100644 index 71656d7c5c8..00000000000 --- a/src/main/java/seedu/address/logic/commands/AddCommand.java +++ /dev/null @@ -1,67 +0,0 @@ -package seedu.address.logic.commands; - -import static java.util.Objects.requireNonNull; -import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; -import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; -import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; -import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; -import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; - -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.model.Model; -import seedu.address.model.person.Person; - -/** - * Adds a person to the address book. - */ -public class AddCommand extends Command { - - public static final String COMMAND_WORD = "add"; - - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a person to the address book. " - + "Parameters: " - + PREFIX_NAME + "NAME " - + PREFIX_PHONE + "PHONE " - + PREFIX_EMAIL + "EMAIL " - + PREFIX_ADDRESS + "ADDRESS " - + "[" + PREFIX_TAG + "TAG]...\n" - + "Example: " + COMMAND_WORD + " " - + PREFIX_NAME + "John Doe " - + PREFIX_PHONE + "98765432 " - + PREFIX_EMAIL + "johnd@example.com " - + PREFIX_ADDRESS + "311, Clementi Ave 2, #02-25 " - + PREFIX_TAG + "friends " - + PREFIX_TAG + "owesMoney"; - - public static final String MESSAGE_SUCCESS = "New person added: %1$s"; - public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book"; - - private final Person toAdd; - - /** - * Creates an AddCommand to add the specified {@code Person} - */ - public AddCommand(Person person) { - requireNonNull(person); - toAdd = person; - } - - @Override - public CommandResult execute(Model model) throws CommandException { - requireNonNull(model); - - if (model.hasPerson(toAdd)) { - throw new CommandException(MESSAGE_DUPLICATE_PERSON); - } - - model.addPerson(toAdd); - return new CommandResult(String.format(MESSAGE_SUCCESS, toAdd)); - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof AddCommand // instanceof handles nulls - && toAdd.equals(((AddCommand) other).toAdd)); - } -} diff --git a/src/main/java/seedu/address/logic/commands/ClearCommand.java b/src/main/java/seedu/address/logic/commands/ClearCommand.java deleted file mode 100644 index 9c86b1fa6e4..00000000000 --- a/src/main/java/seedu/address/logic/commands/ClearCommand.java +++ /dev/null @@ -1,23 +0,0 @@ -package seedu.address.logic.commands; - -import static java.util.Objects.requireNonNull; - -import seedu.address.model.AddressBook; -import seedu.address.model.Model; - -/** - * Clears the address book. - */ -public class ClearCommand extends Command { - - public static final String COMMAND_WORD = "clear"; - public static final String MESSAGE_SUCCESS = "Address book has been cleared!"; - - - @Override - public CommandResult execute(Model model) { - requireNonNull(model); - model.setAddressBook(new AddressBook()); - return new CommandResult(MESSAGE_SUCCESS); - } -} diff --git a/src/main/java/seedu/address/logic/commands/DeleteCommand.java b/src/main/java/seedu/address/logic/commands/DeleteCommand.java deleted file mode 100644 index 02fd256acba..00000000000 --- a/src/main/java/seedu/address/logic/commands/DeleteCommand.java +++ /dev/null @@ -1,53 +0,0 @@ -package seedu.address.logic.commands; - -import static java.util.Objects.requireNonNull; - -import java.util.List; - -import seedu.address.commons.core.Messages; -import seedu.address.commons.core.index.Index; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.model.Model; -import seedu.address.model.person.Person; - -/** - * Deletes a person identified using it's displayed index from the address book. - */ -public class DeleteCommand extends Command { - - public static final String COMMAND_WORD = "delete"; - - public static final String MESSAGE_USAGE = COMMAND_WORD - + ": Deletes the person identified by the index number used in the displayed person list.\n" - + "Parameters: INDEX (must be a positive integer)\n" - + "Example: " + COMMAND_WORD + " 1"; - - public static final String MESSAGE_DELETE_PERSON_SUCCESS = "Deleted Person: %1$s"; - - private final Index targetIndex; - - public DeleteCommand(Index targetIndex) { - this.targetIndex = targetIndex; - } - - @Override - public CommandResult execute(Model model) throws CommandException { - requireNonNull(model); - List lastShownList = model.getFilteredPersonList(); - - if (targetIndex.getZeroBased() >= lastShownList.size()) { - throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); - } - - Person personToDelete = lastShownList.get(targetIndex.getZeroBased()); - model.deletePerson(personToDelete); - return new CommandResult(String.format(MESSAGE_DELETE_PERSON_SUCCESS, personToDelete)); - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof DeleteCommand // instanceof handles nulls - && targetIndex.equals(((DeleteCommand) other).targetIndex)); // state check - } -} diff --git a/src/main/java/seedu/address/logic/commands/EditCommand.java b/src/main/java/seedu/address/logic/commands/EditCommand.java deleted file mode 100644 index 7e36114902f..00000000000 --- a/src/main/java/seedu/address/logic/commands/EditCommand.java +++ /dev/null @@ -1,226 +0,0 @@ -package seedu.address.logic.commands; - -import static java.util.Objects.requireNonNull; -import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; -import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; -import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; -import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; -import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; -import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; - -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Optional; -import java.util.Set; - -import seedu.address.commons.core.Messages; -import seedu.address.commons.core.index.Index; -import seedu.address.commons.util.CollectionUtil; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.model.Model; -import seedu.address.model.person.Address; -import seedu.address.model.person.Email; -import seedu.address.model.person.Name; -import seedu.address.model.person.Person; -import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; - -/** - * Edits the details of an existing person in the address book. - */ -public class EditCommand extends Command { - - public static final String COMMAND_WORD = "edit"; - - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Edits the details of the person identified " - + "by the index number used in the displayed person list. " - + "Existing values will be overwritten by the input values.\n" - + "Parameters: INDEX (must be a positive integer) " - + "[" + PREFIX_NAME + "NAME] " - + "[" + PREFIX_PHONE + "PHONE] " - + "[" + PREFIX_EMAIL + "EMAIL] " - + "[" + PREFIX_ADDRESS + "ADDRESS] " - + "[" + PREFIX_TAG + "TAG]...\n" - + "Example: " + COMMAND_WORD + " 1 " - + PREFIX_PHONE + "91234567 " - + PREFIX_EMAIL + "johndoe@example.com"; - - public static final String MESSAGE_EDIT_PERSON_SUCCESS = "Edited Person: %1$s"; - public static final String MESSAGE_NOT_EDITED = "At least one field to edit must be provided."; - public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book."; - - private final Index index; - private final EditPersonDescriptor editPersonDescriptor; - - /** - * @param index of the person in the filtered person list to edit - * @param editPersonDescriptor details to edit the person with - */ - public EditCommand(Index index, EditPersonDescriptor editPersonDescriptor) { - requireNonNull(index); - requireNonNull(editPersonDescriptor); - - this.index = index; - this.editPersonDescriptor = new EditPersonDescriptor(editPersonDescriptor); - } - - @Override - public CommandResult execute(Model model) throws CommandException { - requireNonNull(model); - List lastShownList = model.getFilteredPersonList(); - - if (index.getZeroBased() >= lastShownList.size()) { - throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); - } - - Person personToEdit = lastShownList.get(index.getZeroBased()); - Person editedPerson = createEditedPerson(personToEdit, editPersonDescriptor); - - if (!personToEdit.isSamePerson(editedPerson) && model.hasPerson(editedPerson)) { - throw new CommandException(MESSAGE_DUPLICATE_PERSON); - } - - model.setPerson(personToEdit, editedPerson); - model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); - return new CommandResult(String.format(MESSAGE_EDIT_PERSON_SUCCESS, editedPerson)); - } - - /** - * Creates and returns a {@code Person} with the details of {@code personToEdit} - * edited with {@code editPersonDescriptor}. - */ - private static Person createEditedPerson(Person personToEdit, EditPersonDescriptor editPersonDescriptor) { - assert personToEdit != null; - - Name updatedName = editPersonDescriptor.getName().orElse(personToEdit.getName()); - Phone updatedPhone = editPersonDescriptor.getPhone().orElse(personToEdit.getPhone()); - Email updatedEmail = editPersonDescriptor.getEmail().orElse(personToEdit.getEmail()); - Address updatedAddress = editPersonDescriptor.getAddress().orElse(personToEdit.getAddress()); - Set updatedTags = editPersonDescriptor.getTags().orElse(personToEdit.getTags()); - - return new Person(updatedName, updatedPhone, updatedEmail, updatedAddress, updatedTags); - } - - @Override - public boolean equals(Object other) { - // short circuit if same object - if (other == this) { - return true; - } - - // instanceof handles nulls - if (!(other instanceof EditCommand)) { - return false; - } - - // state check - EditCommand e = (EditCommand) other; - return index.equals(e.index) - && editPersonDescriptor.equals(e.editPersonDescriptor); - } - - /** - * Stores the details to edit the person with. Each non-empty field value will replace the - * corresponding field value of the person. - */ - public static class EditPersonDescriptor { - private Name name; - private Phone phone; - private Email email; - private Address address; - private Set tags; - - public EditPersonDescriptor() {} - - /** - * Copy constructor. - * A defensive copy of {@code tags} is used internally. - */ - public EditPersonDescriptor(EditPersonDescriptor toCopy) { - setName(toCopy.name); - setPhone(toCopy.phone); - setEmail(toCopy.email); - setAddress(toCopy.address); - setTags(toCopy.tags); - } - - /** - * Returns true if at least one field is edited. - */ - public boolean isAnyFieldEdited() { - return CollectionUtil.isAnyNonNull(name, phone, email, address, tags); - } - - public void setName(Name name) { - this.name = name; - } - - public Optional getName() { - return Optional.ofNullable(name); - } - - public void setPhone(Phone phone) { - this.phone = phone; - } - - public Optional getPhone() { - return Optional.ofNullable(phone); - } - - public void setEmail(Email email) { - this.email = email; - } - - public Optional getEmail() { - return Optional.ofNullable(email); - } - - public void setAddress(Address address) { - this.address = address; - } - - public Optional
getAddress() { - return Optional.ofNullable(address); - } - - /** - * Sets {@code tags} to this object's {@code tags}. - * A defensive copy of {@code tags} is used internally. - */ - public void setTags(Set tags) { - this.tags = (tags != null) ? new HashSet<>(tags) : null; - } - - /** - * Returns an unmodifiable tag set, which throws {@code UnsupportedOperationException} - * if modification is attempted. - * Returns {@code Optional#empty()} if {@code tags} is null. - */ - public Optional> getTags() { - return (tags != null) ? Optional.of(Collections.unmodifiableSet(tags)) : Optional.empty(); - } - - @Override - public boolean equals(Object other) { - // short circuit if same object - if (other == this) { - return true; - } - - // instanceof handles nulls - if (!(other instanceof EditPersonDescriptor)) { - return false; - } - - // state check - EditPersonDescriptor e = (EditPersonDescriptor) other; - - return getName().equals(e.getName()) - && getPhone().equals(e.getPhone()) - && getEmail().equals(e.getEmail()) - && getAddress().equals(e.getAddress()) - && getTags().equals(e.getTags()); - } - } -} diff --git a/src/main/java/seedu/address/logic/commands/ExitCommand.java b/src/main/java/seedu/address/logic/commands/ExitCommand.java deleted file mode 100644 index 3dd85a8ba90..00000000000 --- a/src/main/java/seedu/address/logic/commands/ExitCommand.java +++ /dev/null @@ -1,19 +0,0 @@ -package seedu.address.logic.commands; - -import seedu.address.model.Model; - -/** - * Terminates the program. - */ -public class ExitCommand extends Command { - - public static final String COMMAND_WORD = "exit"; - - public static final String MESSAGE_EXIT_ACKNOWLEDGEMENT = "Exiting Address Book as requested ..."; - - @Override - public CommandResult execute(Model model) { - return new CommandResult(MESSAGE_EXIT_ACKNOWLEDGEMENT, false, true); - } - -} diff --git a/src/main/java/seedu/address/logic/commands/FindCommand.java b/src/main/java/seedu/address/logic/commands/FindCommand.java deleted file mode 100644 index d6b19b0a0de..00000000000 --- a/src/main/java/seedu/address/logic/commands/FindCommand.java +++ /dev/null @@ -1,42 +0,0 @@ -package seedu.address.logic.commands; - -import static java.util.Objects.requireNonNull; - -import seedu.address.commons.core.Messages; -import seedu.address.model.Model; -import seedu.address.model.person.NameContainsKeywordsPredicate; - -/** - * Finds and lists all persons in address book whose name contains any of the argument keywords. - * Keyword matching is case insensitive. - */ -public class FindCommand extends Command { - - public static final String COMMAND_WORD = "find"; - - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all persons whose names contain any of " - + "the specified keywords (case-insensitive) and displays them as a list with index numbers.\n" - + "Parameters: KEYWORD [MORE_KEYWORDS]...\n" - + "Example: " + COMMAND_WORD + " alice bob charlie"; - - private final NameContainsKeywordsPredicate predicate; - - public FindCommand(NameContainsKeywordsPredicate predicate) { - this.predicate = predicate; - } - - @Override - public CommandResult execute(Model model) { - requireNonNull(model); - model.updateFilteredPersonList(predicate); - return new CommandResult( - String.format(Messages.MESSAGE_PERSONS_LISTED_OVERVIEW, model.getFilteredPersonList().size())); - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof FindCommand // instanceof handles nulls - && predicate.equals(((FindCommand) other).predicate)); // state check - } -} diff --git a/src/main/java/seedu/address/logic/commands/ListCommand.java b/src/main/java/seedu/address/logic/commands/ListCommand.java deleted file mode 100644 index 84be6ad2596..00000000000 --- a/src/main/java/seedu/address/logic/commands/ListCommand.java +++ /dev/null @@ -1,24 +0,0 @@ -package seedu.address.logic.commands; - -import static java.util.Objects.requireNonNull; -import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; - -import seedu.address.model.Model; - -/** - * Lists all persons in the address book to the user. - */ -public class ListCommand extends Command { - - public static final String COMMAND_WORD = "list"; - - public static final String MESSAGE_SUCCESS = "Listed all persons"; - - - @Override - public CommandResult execute(Model model) { - requireNonNull(model); - model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); - return new CommandResult(MESSAGE_SUCCESS); - } -} diff --git a/src/main/java/seedu/address/logic/parser/AddCommandParser.java b/src/main/java/seedu/address/logic/parser/AddCommandParser.java deleted file mode 100644 index 3b8bfa035e8..00000000000 --- a/src/main/java/seedu/address/logic/parser/AddCommandParser.java +++ /dev/null @@ -1,60 +0,0 @@ -package seedu.address.logic.parser; - -import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; -import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; -import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; -import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; -import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; -import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; - -import java.util.Set; -import java.util.stream.Stream; - -import seedu.address.logic.commands.AddCommand; -import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.person.Address; -import seedu.address.model.person.Email; -import seedu.address.model.person.Name; -import seedu.address.model.person.Person; -import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; - -/** - * Parses input arguments and creates a new AddCommand object - */ -public class AddCommandParser implements Parser { - - /** - * Parses the given {@code String} of arguments in the context of the AddCommand - * and returns an AddCommand object for execution. - * @throws ParseException if the user input does not conform the expected format - */ - public AddCommand parse(String args) throws ParseException { - ArgumentMultimap argMultimap = - ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG); - - if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_ADDRESS, PREFIX_PHONE, PREFIX_EMAIL) - || !argMultimap.getPreamble().isEmpty()) { - throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE)); - } - - Name name = ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get()); - Phone phone = ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get()); - Email email = ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get()); - Address address = ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get()); - Set tagList = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG)); - - Person person = new Person(name, phone, email, address, tagList); - - return new AddCommand(person); - } - - /** - * Returns true if none of the prefixes contains empty {@code Optional} values in the given - * {@code ArgumentMultimap}. - */ - private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { - return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); - } - -} diff --git a/src/main/java/seedu/address/logic/parser/AddressBookParser.java b/src/main/java/seedu/address/logic/parser/AddressBookParser.java deleted file mode 100644 index 1e466792b46..00000000000 --- a/src/main/java/seedu/address/logic/parser/AddressBookParser.java +++ /dev/null @@ -1,76 +0,0 @@ -package seedu.address.logic.parser; - -import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; -import static seedu.address.commons.core.Messages.MESSAGE_UNKNOWN_COMMAND; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import seedu.address.logic.commands.AddCommand; -import seedu.address.logic.commands.ClearCommand; -import seedu.address.logic.commands.Command; -import seedu.address.logic.commands.DeleteCommand; -import seedu.address.logic.commands.EditCommand; -import seedu.address.logic.commands.ExitCommand; -import seedu.address.logic.commands.FindCommand; -import seedu.address.logic.commands.HelpCommand; -import seedu.address.logic.commands.ListCommand; -import seedu.address.logic.parser.exceptions.ParseException; - -/** - * Parses user input. - */ -public class AddressBookParser { - - /** - * Used for initial separation of command word and args. - */ - private static final Pattern BASIC_COMMAND_FORMAT = Pattern.compile("(?\\S+)(?.*)"); - - /** - * Parses user input into command for execution. - * - * @param userInput full user input string - * @return the command based on the user input - * @throws ParseException if the user input does not conform the expected format - */ - public Command parseCommand(String userInput) throws ParseException { - final Matcher matcher = BASIC_COMMAND_FORMAT.matcher(userInput.trim()); - if (!matcher.matches()) { - throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, HelpCommand.MESSAGE_USAGE)); - } - - final String commandWord = matcher.group("commandWord"); - final String arguments = matcher.group("arguments"); - switch (commandWord) { - - case AddCommand.COMMAND_WORD: - return new AddCommandParser().parse(arguments); - - case EditCommand.COMMAND_WORD: - return new EditCommandParser().parse(arguments); - - case DeleteCommand.COMMAND_WORD: - return new DeleteCommandParser().parse(arguments); - - case ClearCommand.COMMAND_WORD: - return new ClearCommand(); - - case FindCommand.COMMAND_WORD: - return new FindCommandParser().parse(arguments); - - case ListCommand.COMMAND_WORD: - return new ListCommand(); - - case ExitCommand.COMMAND_WORD: - return new ExitCommand(); - - case HelpCommand.COMMAND_WORD: - return new HelpCommand(); - - default: - throw new ParseException(MESSAGE_UNKNOWN_COMMAND); - } - } - -} diff --git a/src/main/java/seedu/address/logic/parser/CliSyntax.java b/src/main/java/seedu/address/logic/parser/CliSyntax.java deleted file mode 100644 index 75b1a9bf119..00000000000 --- a/src/main/java/seedu/address/logic/parser/CliSyntax.java +++ /dev/null @@ -1,15 +0,0 @@ -package seedu.address.logic.parser; - -/** - * Contains Command Line Interface (CLI) syntax definitions common to multiple commands - */ -public class CliSyntax { - - /* Prefix definitions */ - public static final Prefix PREFIX_NAME = new Prefix("n/"); - public static final Prefix PREFIX_PHONE = new Prefix("p/"); - public static final Prefix PREFIX_EMAIL = new Prefix("e/"); - public static final Prefix PREFIX_ADDRESS = new Prefix("a/"); - public static final Prefix PREFIX_TAG = new Prefix("t/"); - -} diff --git a/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java deleted file mode 100644 index 522b93081cc..00000000000 --- a/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java +++ /dev/null @@ -1,29 +0,0 @@ -package seedu.address.logic.parser; - -import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; - -import seedu.address.commons.core.index.Index; -import seedu.address.logic.commands.DeleteCommand; -import seedu.address.logic.parser.exceptions.ParseException; - -/** - * Parses input arguments and creates a new DeleteCommand object - */ -public class DeleteCommandParser implements Parser { - - /** - * Parses the given {@code String} of arguments in the context of the DeleteCommand - * and returns a DeleteCommand object for execution. - * @throws ParseException if the user input does not conform the expected format - */ - public DeleteCommand parse(String args) throws ParseException { - try { - Index index = ParserUtil.parseIndex(args); - return new DeleteCommand(index); - } catch (ParseException pe) { - throw new ParseException( - String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE), pe); - } - } - -} diff --git a/src/main/java/seedu/address/logic/parser/EditCommandParser.java b/src/main/java/seedu/address/logic/parser/EditCommandParser.java deleted file mode 100644 index 845644b7dea..00000000000 --- a/src/main/java/seedu/address/logic/parser/EditCommandParser.java +++ /dev/null @@ -1,82 +0,0 @@ -package seedu.address.logic.parser; - -import static java.util.Objects.requireNonNull; -import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; -import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; -import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; -import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; -import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; -import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; - -import java.util.Collection; -import java.util.Collections; -import java.util.Optional; -import java.util.Set; - -import seedu.address.commons.core.index.Index; -import seedu.address.logic.commands.EditCommand; -import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; -import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.tag.Tag; - -/** - * Parses input arguments and creates a new EditCommand object - */ -public class EditCommandParser implements Parser { - - /** - * Parses the given {@code String} of arguments in the context of the EditCommand - * and returns an EditCommand object for execution. - * @throws ParseException if the user input does not conform the expected format - */ - public EditCommand parse(String args) throws ParseException { - requireNonNull(args); - ArgumentMultimap argMultimap = - ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG); - - Index index; - - try { - index = ParserUtil.parseIndex(argMultimap.getPreamble()); - } catch (ParseException pe) { - throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE), pe); - } - - EditPersonDescriptor editPersonDescriptor = new EditPersonDescriptor(); - if (argMultimap.getValue(PREFIX_NAME).isPresent()) { - editPersonDescriptor.setName(ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get())); - } - if (argMultimap.getValue(PREFIX_PHONE).isPresent()) { - editPersonDescriptor.setPhone(ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get())); - } - if (argMultimap.getValue(PREFIX_EMAIL).isPresent()) { - editPersonDescriptor.setEmail(ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get())); - } - if (argMultimap.getValue(PREFIX_ADDRESS).isPresent()) { - editPersonDescriptor.setAddress(ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get())); - } - parseTagsForEdit(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(editPersonDescriptor::setTags); - - if (!editPersonDescriptor.isAnyFieldEdited()) { - throw new ParseException(EditCommand.MESSAGE_NOT_EDITED); - } - - return new EditCommand(index, editPersonDescriptor); - } - - /** - * Parses {@code Collection tags} into a {@code Set} if {@code tags} is non-empty. - * If {@code tags} contain only one element which is an empty string, it will be parsed into a - * {@code Set} containing zero tags. - */ - private Optional> parseTagsForEdit(Collection tags) throws ParseException { - assert tags != null; - - if (tags.isEmpty()) { - return Optional.empty(); - } - Collection tagSet = tags.size() == 1 && tags.contains("") ? Collections.emptySet() : tags; - return Optional.of(ParserUtil.parseTags(tagSet)); - } - -} diff --git a/src/main/java/seedu/address/logic/parser/ParserUtil.java b/src/main/java/seedu/address/logic/parser/ParserUtil.java deleted file mode 100644 index b117acb9c55..00000000000 --- a/src/main/java/seedu/address/logic/parser/ParserUtil.java +++ /dev/null @@ -1,124 +0,0 @@ -package seedu.address.logic.parser; - -import static java.util.Objects.requireNonNull; - -import java.util.Collection; -import java.util.HashSet; -import java.util.Set; - -import seedu.address.commons.core.index.Index; -import seedu.address.commons.util.StringUtil; -import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.person.Address; -import seedu.address.model.person.Email; -import seedu.address.model.person.Name; -import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; - -/** - * Contains utility methods used for parsing strings in the various *Parser classes. - */ -public class ParserUtil { - - public static final String MESSAGE_INVALID_INDEX = "Index is not a non-zero unsigned integer."; - - /** - * Parses {@code oneBasedIndex} into an {@code Index} and returns it. Leading and trailing whitespaces will be - * trimmed. - * @throws ParseException if the specified index is invalid (not non-zero unsigned integer). - */ - public static Index parseIndex(String oneBasedIndex) throws ParseException { - String trimmedIndex = oneBasedIndex.trim(); - if (!StringUtil.isNonZeroUnsignedInteger(trimmedIndex)) { - throw new ParseException(MESSAGE_INVALID_INDEX); - } - return Index.fromOneBased(Integer.parseInt(trimmedIndex)); - } - - /** - * Parses a {@code String name} into a {@code Name}. - * Leading and trailing whitespaces will be trimmed. - * - * @throws ParseException if the given {@code name} is invalid. - */ - public static Name parseName(String name) throws ParseException { - requireNonNull(name); - String trimmedName = name.trim(); - if (!Name.isValidName(trimmedName)) { - throw new ParseException(Name.MESSAGE_CONSTRAINTS); - } - return new Name(trimmedName); - } - - /** - * Parses a {@code String phone} into a {@code Phone}. - * Leading and trailing whitespaces will be trimmed. - * - * @throws ParseException if the given {@code phone} is invalid. - */ - public static Phone parsePhone(String phone) throws ParseException { - requireNonNull(phone); - String trimmedPhone = phone.trim(); - if (!Phone.isValidPhone(trimmedPhone)) { - throw new ParseException(Phone.MESSAGE_CONSTRAINTS); - } - return new Phone(trimmedPhone); - } - - /** - * Parses a {@code String address} into an {@code Address}. - * Leading and trailing whitespaces will be trimmed. - * - * @throws ParseException if the given {@code address} is invalid. - */ - public static Address parseAddress(String address) throws ParseException { - requireNonNull(address); - String trimmedAddress = address.trim(); - if (!Address.isValidAddress(trimmedAddress)) { - throw new ParseException(Address.MESSAGE_CONSTRAINTS); - } - return new Address(trimmedAddress); - } - - /** - * Parses a {@code String email} into an {@code Email}. - * Leading and trailing whitespaces will be trimmed. - * - * @throws ParseException if the given {@code email} is invalid. - */ - public static Email parseEmail(String email) throws ParseException { - requireNonNull(email); - String trimmedEmail = email.trim(); - if (!Email.isValidEmail(trimmedEmail)) { - throw new ParseException(Email.MESSAGE_CONSTRAINTS); - } - return new Email(trimmedEmail); - } - - /** - * Parses a {@code String tag} into a {@code Tag}. - * Leading and trailing whitespaces will be trimmed. - * - * @throws ParseException if the given {@code tag} is invalid. - */ - public static Tag parseTag(String tag) throws ParseException { - requireNonNull(tag); - String trimmedTag = tag.trim(); - if (!Tag.isValidTagName(trimmedTag)) { - throw new ParseException(Tag.MESSAGE_CONSTRAINTS); - } - return new Tag(trimmedTag); - } - - /** - * Parses {@code Collection tags} into a {@code Set}. - */ - public static Set parseTags(Collection tags) throws ParseException { - requireNonNull(tags); - final Set tagSet = new HashSet<>(); - for (String tagName : tags) { - tagSet.add(parseTag(tagName)); - } - return tagSet; - } -} diff --git a/src/main/java/seedu/address/model/AddressBook.java b/src/main/java/seedu/address/model/AddressBook.java deleted file mode 100644 index 1a943a0781a..00000000000 --- a/src/main/java/seedu/address/model/AddressBook.java +++ /dev/null @@ -1,120 +0,0 @@ -package seedu.address.model; - -import static java.util.Objects.requireNonNull; - -import java.util.List; - -import javafx.collections.ObservableList; -import seedu.address.model.person.Person; -import seedu.address.model.person.UniquePersonList; - -/** - * Wraps all data at the address-book level - * Duplicates are not allowed (by .isSamePerson comparison) - */ -public class AddressBook implements ReadOnlyAddressBook { - - private final UniquePersonList persons; - - /* - * The 'unusual' code block below is a non-static initialization block, sometimes used to avoid duplication - * between constructors. See https://docs.oracle.com/javase/tutorial/java/javaOO/initial.html - * - * Note that non-static init blocks are not recommended to use. There are other ways to avoid duplication - * among constructors. - */ - { - persons = new UniquePersonList(); - } - - public AddressBook() {} - - /** - * Creates an AddressBook using the Persons in the {@code toBeCopied} - */ - public AddressBook(ReadOnlyAddressBook toBeCopied) { - this(); - resetData(toBeCopied); - } - - //// list overwrite operations - - /** - * Replaces the contents of the person list with {@code persons}. - * {@code persons} must not contain duplicate persons. - */ - public void setPersons(List persons) { - this.persons.setPersons(persons); - } - - /** - * Resets the existing data of this {@code AddressBook} with {@code newData}. - */ - public void resetData(ReadOnlyAddressBook newData) { - requireNonNull(newData); - - setPersons(newData.getPersonList()); - } - - //// person-level operations - - /** - * Returns true if a person with the same identity as {@code person} exists in the address book. - */ - public boolean hasPerson(Person person) { - requireNonNull(person); - return persons.contains(person); - } - - /** - * Adds a person to the address book. - * The person must not already exist in the address book. - */ - public void addPerson(Person p) { - persons.add(p); - } - - /** - * Replaces the given person {@code target} in the list with {@code editedPerson}. - * {@code target} must exist in the address book. - * The person identity of {@code editedPerson} must not be the same as another existing person in the address book. - */ - public void setPerson(Person target, Person editedPerson) { - requireNonNull(editedPerson); - - persons.setPerson(target, editedPerson); - } - - /** - * Removes {@code key} from this {@code AddressBook}. - * {@code key} must exist in the address book. - */ - public void removePerson(Person key) { - persons.remove(key); - } - - //// util methods - - @Override - public String toString() { - return persons.asUnmodifiableObservableList().size() + " persons"; - // TODO: refine later - } - - @Override - public ObservableList getPersonList() { - return persons.asUnmodifiableObservableList(); - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof AddressBook // instanceof handles nulls - && persons.equals(((AddressBook) other).persons)); - } - - @Override - public int hashCode() { - return persons.hashCode(); - } -} diff --git a/src/main/java/seedu/address/model/Model.java b/src/main/java/seedu/address/model/Model.java deleted file mode 100644 index d54df471c1f..00000000000 --- a/src/main/java/seedu/address/model/Model.java +++ /dev/null @@ -1,87 +0,0 @@ -package seedu.address.model; - -import java.nio.file.Path; -import java.util.function.Predicate; - -import javafx.collections.ObservableList; -import seedu.address.commons.core.GuiSettings; -import seedu.address.model.person.Person; - -/** - * The API of the Model component. - */ -public interface Model { - /** {@code Predicate} that always evaluate to true */ - Predicate PREDICATE_SHOW_ALL_PERSONS = unused -> true; - - /** - * Replaces user prefs data with the data in {@code userPrefs}. - */ - void setUserPrefs(ReadOnlyUserPrefs userPrefs); - - /** - * Returns the user prefs. - */ - ReadOnlyUserPrefs getUserPrefs(); - - /** - * Returns the user prefs' GUI settings. - */ - GuiSettings getGuiSettings(); - - /** - * Sets the user prefs' GUI settings. - */ - void setGuiSettings(GuiSettings guiSettings); - - /** - * Returns the user prefs' address book file path. - */ - Path getAddressBookFilePath(); - - /** - * Sets the user prefs' address book file path. - */ - void setAddressBookFilePath(Path addressBookFilePath); - - /** - * Replaces address book data with the data in {@code addressBook}. - */ - void setAddressBook(ReadOnlyAddressBook addressBook); - - /** Returns the AddressBook */ - ReadOnlyAddressBook getAddressBook(); - - /** - * Returns true if a person with the same identity as {@code person} exists in the address book. - */ - boolean hasPerson(Person person); - - /** - * Deletes the given person. - * The person must exist in the address book. - */ - void deletePerson(Person target); - - /** - * Adds the given person. - * {@code person} must not already exist in the address book. - */ - void addPerson(Person person); - - /** - * Replaces the given person {@code target} with {@code editedPerson}. - * {@code target} must exist in the address book. - * The person identity of {@code editedPerson} must not be the same as another existing person in the address book. - */ - void setPerson(Person target, Person editedPerson); - - /** Returns an unmodifiable view of the filtered person list */ - ObservableList getFilteredPersonList(); - - /** - * Updates the filter of the filtered person list to filter by the given {@code predicate}. - * @throws NullPointerException if {@code predicate} is null. - */ - void updateFilteredPersonList(Predicate predicate); -} diff --git a/src/main/java/seedu/address/model/ModelManager.java b/src/main/java/seedu/address/model/ModelManager.java deleted file mode 100644 index 86c1df298d7..00000000000 --- a/src/main/java/seedu/address/model/ModelManager.java +++ /dev/null @@ -1,150 +0,0 @@ -package seedu.address.model; - -import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; - -import java.nio.file.Path; -import java.util.function.Predicate; -import java.util.logging.Logger; - -import javafx.collections.ObservableList; -import javafx.collections.transformation.FilteredList; -import seedu.address.commons.core.GuiSettings; -import seedu.address.commons.core.LogsCenter; -import seedu.address.model.person.Person; - -/** - * Represents the in-memory model of the address book data. - */ -public class ModelManager implements Model { - private static final Logger logger = LogsCenter.getLogger(ModelManager.class); - - private final AddressBook addressBook; - private final UserPrefs userPrefs; - private final FilteredList filteredPersons; - - /** - * Initializes a ModelManager with the given addressBook and userPrefs. - */ - public ModelManager(ReadOnlyAddressBook addressBook, ReadOnlyUserPrefs userPrefs) { - requireAllNonNull(addressBook, userPrefs); - - logger.fine("Initializing with address book: " + addressBook + " and user prefs " + userPrefs); - - this.addressBook = new AddressBook(addressBook); - this.userPrefs = new UserPrefs(userPrefs); - filteredPersons = new FilteredList<>(this.addressBook.getPersonList()); - } - - public ModelManager() { - this(new AddressBook(), new UserPrefs()); - } - - //=========== UserPrefs ================================================================================== - - @Override - public void setUserPrefs(ReadOnlyUserPrefs userPrefs) { - requireNonNull(userPrefs); - this.userPrefs.resetData(userPrefs); - } - - @Override - public ReadOnlyUserPrefs getUserPrefs() { - return userPrefs; - } - - @Override - public GuiSettings getGuiSettings() { - return userPrefs.getGuiSettings(); - } - - @Override - public void setGuiSettings(GuiSettings guiSettings) { - requireNonNull(guiSettings); - userPrefs.setGuiSettings(guiSettings); - } - - @Override - public Path getAddressBookFilePath() { - return userPrefs.getAddressBookFilePath(); - } - - @Override - public void setAddressBookFilePath(Path addressBookFilePath) { - requireNonNull(addressBookFilePath); - userPrefs.setAddressBookFilePath(addressBookFilePath); - } - - //=========== AddressBook ================================================================================ - - @Override - public void setAddressBook(ReadOnlyAddressBook addressBook) { - this.addressBook.resetData(addressBook); - } - - @Override - public ReadOnlyAddressBook getAddressBook() { - return addressBook; - } - - @Override - public boolean hasPerson(Person person) { - requireNonNull(person); - return addressBook.hasPerson(person); - } - - @Override - public void deletePerson(Person target) { - addressBook.removePerson(target); - } - - @Override - public void addPerson(Person person) { - addressBook.addPerson(person); - updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); - } - - @Override - public void setPerson(Person target, Person editedPerson) { - requireAllNonNull(target, editedPerson); - - addressBook.setPerson(target, editedPerson); - } - - //=========== Filtered Person List Accessors ============================================================= - - /** - * Returns an unmodifiable view of the list of {@code Person} backed by the internal list of - * {@code versionedAddressBook} - */ - @Override - public ObservableList getFilteredPersonList() { - return filteredPersons; - } - - @Override - public void updateFilteredPersonList(Predicate predicate) { - requireNonNull(predicate); - filteredPersons.setPredicate(predicate); - } - - @Override - public boolean equals(Object obj) { - // short circuit if same object - if (obj == this) { - return true; - } - - // instanceof handles nulls - if (!(obj instanceof ModelManager)) { - return false; - } - - // state check - ModelManager other = (ModelManager) obj; - return addressBook.equals(other.addressBook) - && userPrefs.equals(other.userPrefs) - && filteredPersons.equals(other.filteredPersons); - } - -} diff --git a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java b/src/main/java/seedu/address/model/ReadOnlyAddressBook.java deleted file mode 100644 index 6ddc2cd9a29..00000000000 --- a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java +++ /dev/null @@ -1,17 +0,0 @@ -package seedu.address.model; - -import javafx.collections.ObservableList; -import seedu.address.model.person.Person; - -/** - * Unmodifiable view of an address book - */ -public interface ReadOnlyAddressBook { - - /** - * Returns an unmodifiable view of the persons list. - * This list will not contain any duplicate persons. - */ - ObservableList getPersonList(); - -} diff --git a/src/main/java/seedu/address/model/person/Address.java b/src/main/java/seedu/address/model/person/Address.java deleted file mode 100644 index 60472ca22a0..00000000000 --- a/src/main/java/seedu/address/model/person/Address.java +++ /dev/null @@ -1,57 +0,0 @@ -package seedu.address.model.person; - -import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; - -/** - * Represents a Person's address in the address book. - * Guarantees: immutable; is valid as declared in {@link #isValidAddress(String)} - */ -public class Address { - - public static final String MESSAGE_CONSTRAINTS = "Addresses can take any values, and it should not be blank"; - - /* - * The first character of the address must not be a whitespace, - * otherwise " " (a blank string) becomes a valid input. - */ - public static final String VALIDATION_REGEX = "[^\\s].*"; - - public final String value; - - /** - * Constructs an {@code Address}. - * - * @param address A valid address. - */ - public Address(String address) { - requireNonNull(address); - checkArgument(isValidAddress(address), MESSAGE_CONSTRAINTS); - value = address; - } - - /** - * Returns true if a given string is a valid email. - */ - public static boolean isValidAddress(String test) { - return test.matches(VALIDATION_REGEX); - } - - @Override - public String toString() { - return value; - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof Address // instanceof handles nulls - && value.equals(((Address) other).value)); // state check - } - - @Override - public int hashCode() { - return value.hashCode(); - } - -} diff --git a/src/main/java/seedu/address/model/person/Email.java b/src/main/java/seedu/address/model/person/Email.java deleted file mode 100644 index f866e7133de..00000000000 --- a/src/main/java/seedu/address/model/person/Email.java +++ /dev/null @@ -1,71 +0,0 @@ -package seedu.address.model.person; - -import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; - -/** - * Represents a Person's email in the address book. - * Guarantees: immutable; is valid as declared in {@link #isValidEmail(String)} - */ -public class Email { - - private static final String SPECIAL_CHARACTERS = "+_.-"; - public static final String MESSAGE_CONSTRAINTS = "Emails should be of the format local-part@domain " - + "and adhere to the following constraints:\n" - + "1. The local-part should only contain alphanumeric characters and these special characters, excluding " - + "the parentheses, (" + SPECIAL_CHARACTERS + "). The local-part may not start or end with any special " - + "characters.\n" - + "2. This is followed by a '@' and then a domain name. The domain name is made up of domain labels " - + "separated by periods.\n" - + "The domain name must:\n" - + " - end with a domain label at least 2 characters long\n" - + " - have each domain label start and end with alphanumeric characters\n" - + " - have each domain label consist of alphanumeric characters, separated only by hyphens, if any."; - // alphanumeric and special characters - private static final String ALPHANUMERIC_NO_UNDERSCORE = "[^\\W_]+"; // alphanumeric characters except underscore - private static final String LOCAL_PART_REGEX = "^" + ALPHANUMERIC_NO_UNDERSCORE + "([" + SPECIAL_CHARACTERS + "]" - + ALPHANUMERIC_NO_UNDERSCORE + ")*"; - private static final String DOMAIN_PART_REGEX = ALPHANUMERIC_NO_UNDERSCORE - + "(-" + ALPHANUMERIC_NO_UNDERSCORE + ")*"; - private static final String DOMAIN_LAST_PART_REGEX = "(" + DOMAIN_PART_REGEX + "){2,}$"; // At least two chars - private static final String DOMAIN_REGEX = "(" + DOMAIN_PART_REGEX + "\\.)*" + DOMAIN_LAST_PART_REGEX; - public static final String VALIDATION_REGEX = LOCAL_PART_REGEX + "@" + DOMAIN_REGEX; - - public final String value; - - /** - * Constructs an {@code Email}. - * - * @param email A valid email address. - */ - public Email(String email) { - requireNonNull(email); - checkArgument(isValidEmail(email), MESSAGE_CONSTRAINTS); - value = email; - } - - /** - * Returns if a given string is a valid email. - */ - public static boolean isValidEmail(String test) { - return test.matches(VALIDATION_REGEX); - } - - @Override - public String toString() { - return value; - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof Email // instanceof handles nulls - && value.equals(((Email) other).value)); // state check - } - - @Override - public int hashCode() { - return value.hashCode(); - } - -} diff --git a/src/main/java/seedu/address/model/person/Name.java b/src/main/java/seedu/address/model/person/Name.java deleted file mode 100644 index 79244d71cf7..00000000000 --- a/src/main/java/seedu/address/model/person/Name.java +++ /dev/null @@ -1,59 +0,0 @@ -package seedu.address.model.person; - -import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; - -/** - * Represents a Person's name in the address book. - * Guarantees: immutable; is valid as declared in {@link #isValidName(String)} - */ -public class Name { - - public static final String MESSAGE_CONSTRAINTS = - "Names should only contain alphanumeric characters and spaces, and it should not be blank"; - - /* - * The first character of the address must not be a whitespace, - * otherwise " " (a blank string) becomes a valid input. - */ - public static final String VALIDATION_REGEX = "[\\p{Alnum}][\\p{Alnum} ]*"; - - public final String fullName; - - /** - * Constructs a {@code Name}. - * - * @param name A valid name. - */ - public Name(String name) { - requireNonNull(name); - checkArgument(isValidName(name), MESSAGE_CONSTRAINTS); - fullName = name; - } - - /** - * Returns true if a given string is a valid name. - */ - public static boolean isValidName(String test) { - return test.matches(VALIDATION_REGEX); - } - - - @Override - public String toString() { - return fullName; - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof Name // instanceof handles nulls - && fullName.equals(((Name) other).fullName)); // state check - } - - @Override - public int hashCode() { - return fullName.hashCode(); - } - -} diff --git a/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java deleted file mode 100644 index c9b5868427c..00000000000 --- a/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java +++ /dev/null @@ -1,31 +0,0 @@ -package seedu.address.model.person; - -import java.util.List; -import java.util.function.Predicate; - -import seedu.address.commons.util.StringUtil; - -/** - * Tests that a {@code Person}'s {@code Name} matches any of the keywords given. - */ -public class NameContainsKeywordsPredicate implements Predicate { - private final List keywords; - - public NameContainsKeywordsPredicate(List keywords) { - this.keywords = keywords; - } - - @Override - public boolean test(Person person) { - return keywords.stream() - .anyMatch(keyword -> StringUtil.containsWordIgnoreCase(person.getName().fullName, keyword)); - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof NameContainsKeywordsPredicate // instanceof handles nulls - && keywords.equals(((NameContainsKeywordsPredicate) other).keywords)); // state check - } - -} diff --git a/src/main/java/seedu/address/model/person/Person.java b/src/main/java/seedu/address/model/person/Person.java deleted file mode 100644 index 8ff1d83fe89..00000000000 --- a/src/main/java/seedu/address/model/person/Person.java +++ /dev/null @@ -1,123 +0,0 @@ -package seedu.address.model.person; - -import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; - -import java.util.Collections; -import java.util.HashSet; -import java.util.Objects; -import java.util.Set; - -import seedu.address.model.tag.Tag; - -/** - * Represents a Person in the address book. - * Guarantees: details are present and not null, field values are validated, immutable. - */ -public class Person { - - // Identity fields - private final Name name; - private final Phone phone; - private final Email email; - - // Data fields - private final Address address; - private final Set tags = new HashSet<>(); - - /** - * Every field must be present and not null. - */ - public Person(Name name, Phone phone, Email email, Address address, Set tags) { - requireAllNonNull(name, phone, email, address, tags); - this.name = name; - this.phone = phone; - this.email = email; - this.address = address; - this.tags.addAll(tags); - } - - public Name getName() { - return name; - } - - public Phone getPhone() { - return phone; - } - - public Email getEmail() { - return email; - } - - public Address getAddress() { - return address; - } - - /** - * Returns an immutable tag set, which throws {@code UnsupportedOperationException} - * if modification is attempted. - */ - public Set getTags() { - return Collections.unmodifiableSet(tags); - } - - /** - * Returns true if both persons have the same name. - * This defines a weaker notion of equality between two persons. - */ - public boolean isSamePerson(Person otherPerson) { - if (otherPerson == this) { - return true; - } - - return otherPerson != null - && otherPerson.getName().equals(getName()); - } - - /** - * Returns true if both persons have the same identity and data fields. - * This defines a stronger notion of equality between two persons. - */ - @Override - public boolean equals(Object other) { - if (other == this) { - return true; - } - - if (!(other instanceof Person)) { - return false; - } - - Person otherPerson = (Person) other; - return otherPerson.getName().equals(getName()) - && otherPerson.getPhone().equals(getPhone()) - && otherPerson.getEmail().equals(getEmail()) - && otherPerson.getAddress().equals(getAddress()) - && otherPerson.getTags().equals(getTags()); - } - - @Override - public int hashCode() { - // use this method for custom fields hashing instead of implementing your own - return Objects.hash(name, phone, email, address, tags); - } - - @Override - public String toString() { - final StringBuilder builder = new StringBuilder(); - builder.append(getName()) - .append("; Phone: ") - .append(getPhone()) - .append("; Email: ") - .append(getEmail()) - .append("; Address: ") - .append(getAddress()); - - Set tags = getTags(); - if (!tags.isEmpty()) { - builder.append("; Tags: "); - tags.forEach(builder::append); - } - return builder.toString(); - } - -} diff --git a/src/main/java/seedu/address/model/person/Phone.java b/src/main/java/seedu/address/model/person/Phone.java deleted file mode 100644 index 872c76b382f..00000000000 --- a/src/main/java/seedu/address/model/person/Phone.java +++ /dev/null @@ -1,53 +0,0 @@ -package seedu.address.model.person; - -import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; - -/** - * Represents a Person's phone number in the address book. - * Guarantees: immutable; is valid as declared in {@link #isValidPhone(String)} - */ -public class Phone { - - - public static final String MESSAGE_CONSTRAINTS = - "Phone numbers should only contain numbers, and it should be at least 3 digits long"; - public static final String VALIDATION_REGEX = "\\d{3,}"; - public final String value; - - /** - * Constructs a {@code Phone}. - * - * @param phone A valid phone number. - */ - public Phone(String phone) { - requireNonNull(phone); - checkArgument(isValidPhone(phone), MESSAGE_CONSTRAINTS); - value = phone; - } - - /** - * Returns true if a given string is a valid phone number. - */ - public static boolean isValidPhone(String test) { - return test.matches(VALIDATION_REGEX); - } - - @Override - public String toString() { - return value; - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof Phone // instanceof handles nulls - && value.equals(((Phone) other).value)); // state check - } - - @Override - public int hashCode() { - return value.hashCode(); - } - -} diff --git a/src/main/java/seedu/address/model/person/UniquePersonList.java b/src/main/java/seedu/address/model/person/UniquePersonList.java deleted file mode 100644 index 0fee4fe57e6..00000000000 --- a/src/main/java/seedu/address/model/person/UniquePersonList.java +++ /dev/null @@ -1,137 +0,0 @@ -package seedu.address.model.person; - -import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; - -import java.util.Iterator; -import java.util.List; - -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; -import seedu.address.model.person.exceptions.DuplicatePersonException; -import seedu.address.model.person.exceptions.PersonNotFoundException; - -/** - * A list of persons that enforces uniqueness between its elements and does not allow nulls. - * A person is considered unique by comparing using {@code Person#isSamePerson(Person)}. As such, adding and updating of - * persons uses Person#isSamePerson(Person) for equality so as to ensure that the person being added or updated is - * unique in terms of identity in the UniquePersonList. However, the removal of a person uses Person#equals(Object) so - * as to ensure that the person with exactly the same fields will be removed. - * - * Supports a minimal set of list operations. - * - * @see Person#isSamePerson(Person) - */ -public class UniquePersonList implements Iterable { - - private final ObservableList internalList = FXCollections.observableArrayList(); - private final ObservableList internalUnmodifiableList = - FXCollections.unmodifiableObservableList(internalList); - - /** - * Returns true if the list contains an equivalent person as the given argument. - */ - public boolean contains(Person toCheck) { - requireNonNull(toCheck); - return internalList.stream().anyMatch(toCheck::isSamePerson); - } - - /** - * Adds a person to the list. - * The person must not already exist in the list. - */ - public void add(Person toAdd) { - requireNonNull(toAdd); - if (contains(toAdd)) { - throw new DuplicatePersonException(); - } - internalList.add(toAdd); - } - - /** - * Replaces the person {@code target} in the list with {@code editedPerson}. - * {@code target} must exist in the list. - * The person identity of {@code editedPerson} must not be the same as another existing person in the list. - */ - public void setPerson(Person target, Person editedPerson) { - requireAllNonNull(target, editedPerson); - - int index = internalList.indexOf(target); - if (index == -1) { - throw new PersonNotFoundException(); - } - - if (!target.isSamePerson(editedPerson) && contains(editedPerson)) { - throw new DuplicatePersonException(); - } - - internalList.set(index, editedPerson); - } - - /** - * Removes the equivalent person from the list. - * The person must exist in the list. - */ - public void remove(Person toRemove) { - requireNonNull(toRemove); - if (!internalList.remove(toRemove)) { - throw new PersonNotFoundException(); - } - } - - public void setPersons(UniquePersonList replacement) { - requireNonNull(replacement); - internalList.setAll(replacement.internalList); - } - - /** - * Replaces the contents of this list with {@code persons}. - * {@code persons} must not contain duplicate persons. - */ - public void setPersons(List persons) { - requireAllNonNull(persons); - if (!personsAreUnique(persons)) { - throw new DuplicatePersonException(); - } - - internalList.setAll(persons); - } - - /** - * Returns the backing list as an unmodifiable {@code ObservableList}. - */ - public ObservableList asUnmodifiableObservableList() { - return internalUnmodifiableList; - } - - @Override - public Iterator iterator() { - return internalList.iterator(); - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof UniquePersonList // instanceof handles nulls - && internalList.equals(((UniquePersonList) other).internalList)); - } - - @Override - public int hashCode() { - return internalList.hashCode(); - } - - /** - * Returns true if {@code persons} contains only unique persons. - */ - private boolean personsAreUnique(List persons) { - for (int i = 0; i < persons.size() - 1; i++) { - for (int j = i + 1; j < persons.size(); j++) { - if (persons.get(i).isSamePerson(persons.get(j))) { - return false; - } - } - } - return true; - } -} diff --git a/src/main/java/seedu/address/model/person/exceptions/DuplicatePersonException.java b/src/main/java/seedu/address/model/person/exceptions/DuplicatePersonException.java deleted file mode 100644 index d7290f59442..00000000000 --- a/src/main/java/seedu/address/model/person/exceptions/DuplicatePersonException.java +++ /dev/null @@ -1,11 +0,0 @@ -package seedu.address.model.person.exceptions; - -/** - * Signals that the operation will result in duplicate Persons (Persons are considered duplicates if they have the same - * identity). - */ -public class DuplicatePersonException extends RuntimeException { - public DuplicatePersonException() { - super("Operation would result in duplicate persons"); - } -} diff --git a/src/main/java/seedu/address/model/person/exceptions/PersonNotFoundException.java b/src/main/java/seedu/address/model/person/exceptions/PersonNotFoundException.java deleted file mode 100644 index fa764426ca7..00000000000 --- a/src/main/java/seedu/address/model/person/exceptions/PersonNotFoundException.java +++ /dev/null @@ -1,6 +0,0 @@ -package seedu.address.model.person.exceptions; - -/** - * Signals that the operation is unable to find the specified person. - */ -public class PersonNotFoundException extends RuntimeException {} diff --git a/src/main/java/seedu/address/model/tag/Tag.java b/src/main/java/seedu/address/model/tag/Tag.java deleted file mode 100644 index b0ea7e7dad7..00000000000 --- a/src/main/java/seedu/address/model/tag/Tag.java +++ /dev/null @@ -1,54 +0,0 @@ -package seedu.address.model.tag; - -import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; - -/** - * Represents a Tag in the address book. - * Guarantees: immutable; name is valid as declared in {@link #isValidTagName(String)} - */ -public class Tag { - - public static final String MESSAGE_CONSTRAINTS = "Tags names should be alphanumeric"; - public static final String VALIDATION_REGEX = "\\p{Alnum}+"; - - public final String tagName; - - /** - * Constructs a {@code Tag}. - * - * @param tagName A valid tag name. - */ - public Tag(String tagName) { - requireNonNull(tagName); - checkArgument(isValidTagName(tagName), MESSAGE_CONSTRAINTS); - this.tagName = tagName; - } - - /** - * Returns true if a given string is a valid tag name. - */ - public static boolean isValidTagName(String test) { - return test.matches(VALIDATION_REGEX); - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof Tag // instanceof handles nulls - && tagName.equals(((Tag) other).tagName)); // state check - } - - @Override - public int hashCode() { - return tagName.hashCode(); - } - - /** - * Format state as text for viewing. - */ - public String toString() { - return '[' + tagName + ']'; - } - -} diff --git a/src/main/java/seedu/address/model/util/SampleDataUtil.java b/src/main/java/seedu/address/model/util/SampleDataUtil.java deleted file mode 100644 index 1806da4facf..00000000000 --- a/src/main/java/seedu/address/model/util/SampleDataUtil.java +++ /dev/null @@ -1,60 +0,0 @@ -package seedu.address.model.util; - -import java.util.Arrays; -import java.util.Set; -import java.util.stream.Collectors; - -import seedu.address.model.AddressBook; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.person.Address; -import seedu.address.model.person.Email; -import seedu.address.model.person.Name; -import seedu.address.model.person.Person; -import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; - -/** - * Contains utility methods for populating {@code AddressBook} with sample data. - */ -public class SampleDataUtil { - public static Person[] getSamplePersons() { - return new Person[] { - new Person(new Name("Alex Yeoh"), new Phone("87438807"), new Email("alexyeoh@example.com"), - new Address("Blk 30 Geylang Street 29, #06-40"), - getTagSet("friends")), - new Person(new Name("Bernice Yu"), new Phone("99272758"), new Email("berniceyu@example.com"), - new Address("Blk 30 Lorong 3 Serangoon Gardens, #07-18"), - getTagSet("colleagues", "friends")), - new Person(new Name("Charlotte Oliveiro"), new Phone("93210283"), new Email("charlotte@example.com"), - new Address("Blk 11 Ang Mo Kio Street 74, #11-04"), - getTagSet("neighbours")), - new Person(new Name("David Li"), new Phone("91031282"), new Email("lidavid@example.com"), - new Address("Blk 436 Serangoon Gardens Street 26, #16-43"), - getTagSet("family")), - new Person(new Name("Irfan Ibrahim"), new Phone("92492021"), new Email("irfan@example.com"), - new Address("Blk 47 Tampines Street 20, #17-35"), - getTagSet("classmates")), - new Person(new Name("Roy Balakrishnan"), new Phone("92624417"), new Email("royb@example.com"), - new Address("Blk 45 Aljunied Street 85, #11-31"), - getTagSet("colleagues")) - }; - } - - public static ReadOnlyAddressBook getSampleAddressBook() { - AddressBook sampleAb = new AddressBook(); - for (Person samplePerson : getSamplePersons()) { - sampleAb.addPerson(samplePerson); - } - return sampleAb; - } - - /** - * Returns a tag set containing the list of strings given. - */ - public static Set getTagSet(String... strings) { - return Arrays.stream(strings) - .map(Tag::new) - .collect(Collectors.toSet()); - } - -} diff --git a/src/main/java/seedu/address/storage/AddressBookStorage.java b/src/main/java/seedu/address/storage/AddressBookStorage.java deleted file mode 100644 index 4599182b3f9..00000000000 --- a/src/main/java/seedu/address/storage/AddressBookStorage.java +++ /dev/null @@ -1,45 +0,0 @@ -package seedu.address.storage; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.Optional; - -import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.model.ReadOnlyAddressBook; - -/** - * Represents a storage for {@link seedu.address.model.AddressBook}. - */ -public interface AddressBookStorage { - - /** - * Returns the file path of the data file. - */ - Path getAddressBookFilePath(); - - /** - * Returns AddressBook data as a {@link ReadOnlyAddressBook}. - * Returns {@code Optional.empty()} if storage file is not found. - * @throws DataConversionException if the data in storage is not in the expected format. - * @throws IOException if there was any problem when reading from the storage. - */ - Optional readAddressBook() throws DataConversionException, IOException; - - /** - * @see #getAddressBookFilePath() - */ - Optional readAddressBook(Path filePath) throws DataConversionException, IOException; - - /** - * Saves the given {@link ReadOnlyAddressBook} to the storage. - * @param addressBook cannot be null. - * @throws IOException if there was any problem writing to the file. - */ - void saveAddressBook(ReadOnlyAddressBook addressBook) throws IOException; - - /** - * @see #saveAddressBook(ReadOnlyAddressBook) - */ - void saveAddressBook(ReadOnlyAddressBook addressBook, Path filePath) throws IOException; - -} diff --git a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java b/src/main/java/seedu/address/storage/JsonAdaptedPerson.java deleted file mode 100644 index a6321cec2ea..00000000000 --- a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java +++ /dev/null @@ -1,109 +0,0 @@ -package seedu.address.storage; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; - -import seedu.address.commons.exceptions.IllegalValueException; -import seedu.address.model.person.Address; -import seedu.address.model.person.Email; -import seedu.address.model.person.Name; -import seedu.address.model.person.Person; -import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; - -/** - * Jackson-friendly version of {@link Person}. - */ -class JsonAdaptedPerson { - - public static final String MISSING_FIELD_MESSAGE_FORMAT = "Person's %s field is missing!"; - - private final String name; - private final String phone; - private final String email; - private final String address; - private final List tagged = new ArrayList<>(); - - /** - * Constructs a {@code JsonAdaptedPerson} with the given person details. - */ - @JsonCreator - public JsonAdaptedPerson(@JsonProperty("name") String name, @JsonProperty("phone") String phone, - @JsonProperty("email") String email, @JsonProperty("address") String address, - @JsonProperty("tagged") List tagged) { - this.name = name; - this.phone = phone; - this.email = email; - this.address = address; - if (tagged != null) { - this.tagged.addAll(tagged); - } - } - - /** - * Converts a given {@code Person} into this class for Jackson use. - */ - public JsonAdaptedPerson(Person source) { - name = source.getName().fullName; - phone = source.getPhone().value; - email = source.getEmail().value; - address = source.getAddress().value; - tagged.addAll(source.getTags().stream() - .map(JsonAdaptedTag::new) - .collect(Collectors.toList())); - } - - /** - * Converts this Jackson-friendly adapted person object into the model's {@code Person} object. - * - * @throws IllegalValueException if there were any data constraints violated in the adapted person. - */ - public Person toModelType() throws IllegalValueException { - final List personTags = new ArrayList<>(); - for (JsonAdaptedTag tag : tagged) { - personTags.add(tag.toModelType()); - } - - if (name == null) { - throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Name.class.getSimpleName())); - } - if (!Name.isValidName(name)) { - throw new IllegalValueException(Name.MESSAGE_CONSTRAINTS); - } - final Name modelName = new Name(name); - - if (phone == null) { - throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Phone.class.getSimpleName())); - } - if (!Phone.isValidPhone(phone)) { - throw new IllegalValueException(Phone.MESSAGE_CONSTRAINTS); - } - final Phone modelPhone = new Phone(phone); - - if (email == null) { - throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Email.class.getSimpleName())); - } - if (!Email.isValidEmail(email)) { - throw new IllegalValueException(Email.MESSAGE_CONSTRAINTS); - } - final Email modelEmail = new Email(email); - - if (address == null) { - throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Address.class.getSimpleName())); - } - if (!Address.isValidAddress(address)) { - throw new IllegalValueException(Address.MESSAGE_CONSTRAINTS); - } - final Address modelAddress = new Address(address); - - final Set modelTags = new HashSet<>(personTags); - return new Person(modelName, modelPhone, modelEmail, modelAddress, modelTags); - } - -} diff --git a/src/main/java/seedu/address/storage/JsonAdaptedTag.java b/src/main/java/seedu/address/storage/JsonAdaptedTag.java deleted file mode 100644 index 0df22bdb754..00000000000 --- a/src/main/java/seedu/address/storage/JsonAdaptedTag.java +++ /dev/null @@ -1,48 +0,0 @@ -package seedu.address.storage; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonValue; - -import seedu.address.commons.exceptions.IllegalValueException; -import seedu.address.model.tag.Tag; - -/** - * Jackson-friendly version of {@link Tag}. - */ -class JsonAdaptedTag { - - private final String tagName; - - /** - * Constructs a {@code JsonAdaptedTag} with the given {@code tagName}. - */ - @JsonCreator - public JsonAdaptedTag(String tagName) { - this.tagName = tagName; - } - - /** - * Converts a given {@code Tag} into this class for Jackson use. - */ - public JsonAdaptedTag(Tag source) { - tagName = source.tagName; - } - - @JsonValue - public String getTagName() { - return tagName; - } - - /** - * Converts this Jackson-friendly adapted tag object into the model's {@code Tag} object. - * - * @throws IllegalValueException if there were any data constraints violated in the adapted tag. - */ - public Tag toModelType() throws IllegalValueException { - if (!Tag.isValidTagName(tagName)) { - throw new IllegalValueException(Tag.MESSAGE_CONSTRAINTS); - } - return new Tag(tagName); - } - -} diff --git a/src/main/java/seedu/address/storage/JsonAddressBookStorage.java b/src/main/java/seedu/address/storage/JsonAddressBookStorage.java deleted file mode 100644 index dfab9daaa0d..00000000000 --- a/src/main/java/seedu/address/storage/JsonAddressBookStorage.java +++ /dev/null @@ -1,80 +0,0 @@ -package seedu.address.storage; - -import static java.util.Objects.requireNonNull; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.Optional; -import java.util.logging.Logger; - -import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.commons.exceptions.IllegalValueException; -import seedu.address.commons.util.FileUtil; -import seedu.address.commons.util.JsonUtil; -import seedu.address.model.ReadOnlyAddressBook; - -/** - * A class to access AddressBook data stored as a json file on the hard disk. - */ -public class JsonAddressBookStorage implements AddressBookStorage { - - private static final Logger logger = LogsCenter.getLogger(JsonAddressBookStorage.class); - - private Path filePath; - - public JsonAddressBookStorage(Path filePath) { - this.filePath = filePath; - } - - public Path getAddressBookFilePath() { - return filePath; - } - - @Override - public Optional readAddressBook() throws DataConversionException { - return readAddressBook(filePath); - } - - /** - * Similar to {@link #readAddressBook()}. - * - * @param filePath location of the data. Cannot be null. - * @throws DataConversionException if the file is not in the correct format. - */ - public Optional readAddressBook(Path filePath) throws DataConversionException { - requireNonNull(filePath); - - Optional jsonAddressBook = JsonUtil.readJsonFile( - filePath, JsonSerializableAddressBook.class); - if (!jsonAddressBook.isPresent()) { - return Optional.empty(); - } - - try { - return Optional.of(jsonAddressBook.get().toModelType()); - } catch (IllegalValueException ive) { - logger.info("Illegal values found in " + filePath + ": " + ive.getMessage()); - throw new DataConversionException(ive); - } - } - - @Override - public void saveAddressBook(ReadOnlyAddressBook addressBook) throws IOException { - saveAddressBook(addressBook, filePath); - } - - /** - * Similar to {@link #saveAddressBook(ReadOnlyAddressBook)}. - * - * @param filePath location of the data. Cannot be null. - */ - public void saveAddressBook(ReadOnlyAddressBook addressBook, Path filePath) throws IOException { - requireNonNull(addressBook); - requireNonNull(filePath); - - FileUtil.createIfMissing(filePath); - JsonUtil.saveJsonFile(new JsonSerializableAddressBook(addressBook), filePath); - } - -} diff --git a/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java b/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java deleted file mode 100644 index 5efd834091d..00000000000 --- a/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java +++ /dev/null @@ -1,60 +0,0 @@ -package seedu.address.storage; - -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonRootName; - -import seedu.address.commons.exceptions.IllegalValueException; -import seedu.address.model.AddressBook; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.person.Person; - -/** - * An Immutable AddressBook that is serializable to JSON format. - */ -@JsonRootName(value = "addressbook") -class JsonSerializableAddressBook { - - public static final String MESSAGE_DUPLICATE_PERSON = "Persons list contains duplicate person(s)."; - - private final List persons = new ArrayList<>(); - - /** - * Constructs a {@code JsonSerializableAddressBook} with the given persons. - */ - @JsonCreator - public JsonSerializableAddressBook(@JsonProperty("persons") List persons) { - this.persons.addAll(persons); - } - - /** - * Converts a given {@code ReadOnlyAddressBook} into this class for Jackson use. - * - * @param source future changes to this will not affect the created {@code JsonSerializableAddressBook}. - */ - public JsonSerializableAddressBook(ReadOnlyAddressBook source) { - persons.addAll(source.getPersonList().stream().map(JsonAdaptedPerson::new).collect(Collectors.toList())); - } - - /** - * Converts this address book into the model's {@code AddressBook} object. - * - * @throws IllegalValueException if there were any data constraints violated. - */ - public AddressBook toModelType() throws IllegalValueException { - AddressBook addressBook = new AddressBook(); - for (JsonAdaptedPerson jsonAdaptedPerson : persons) { - Person person = jsonAdaptedPerson.toModelType(); - if (addressBook.hasPerson(person)) { - throw new IllegalValueException(MESSAGE_DUPLICATE_PERSON); - } - addressBook.addPerson(person); - } - return addressBook; - } - -} diff --git a/src/main/java/seedu/address/storage/Storage.java b/src/main/java/seedu/address/storage/Storage.java deleted file mode 100644 index beda8bd9f11..00000000000 --- a/src/main/java/seedu/address/storage/Storage.java +++ /dev/null @@ -1,32 +0,0 @@ -package seedu.address.storage; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.Optional; - -import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.ReadOnlyUserPrefs; -import seedu.address.model.UserPrefs; - -/** - * API of the Storage component - */ -public interface Storage extends AddressBookStorage, UserPrefsStorage { - - @Override - Optional readUserPrefs() throws DataConversionException, IOException; - - @Override - void saveUserPrefs(ReadOnlyUserPrefs userPrefs) throws IOException; - - @Override - Path getAddressBookFilePath(); - - @Override - Optional readAddressBook() throws DataConversionException, IOException; - - @Override - void saveAddressBook(ReadOnlyAddressBook addressBook) throws IOException; - -} diff --git a/src/main/java/seedu/address/storage/StorageManager.java b/src/main/java/seedu/address/storage/StorageManager.java deleted file mode 100644 index 6cfa0162164..00000000000 --- a/src/main/java/seedu/address/storage/StorageManager.java +++ /dev/null @@ -1,78 +0,0 @@ -package seedu.address.storage; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.Optional; -import java.util.logging.Logger; - -import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.ReadOnlyUserPrefs; -import seedu.address.model.UserPrefs; - -/** - * Manages storage of AddressBook data in local storage. - */ -public class StorageManager implements Storage { - - private static final Logger logger = LogsCenter.getLogger(StorageManager.class); - private AddressBookStorage addressBookStorage; - private UserPrefsStorage userPrefsStorage; - - /** - * Creates a {@code StorageManager} with the given {@code AddressBookStorage} and {@code UserPrefStorage}. - */ - public StorageManager(AddressBookStorage addressBookStorage, UserPrefsStorage userPrefsStorage) { - this.addressBookStorage = addressBookStorage; - this.userPrefsStorage = userPrefsStorage; - } - - // ================ UserPrefs methods ============================== - - @Override - public Path getUserPrefsFilePath() { - return userPrefsStorage.getUserPrefsFilePath(); - } - - @Override - public Optional readUserPrefs() throws DataConversionException, IOException { - return userPrefsStorage.readUserPrefs(); - } - - @Override - public void saveUserPrefs(ReadOnlyUserPrefs userPrefs) throws IOException { - userPrefsStorage.saveUserPrefs(userPrefs); - } - - - // ================ AddressBook methods ============================== - - @Override - public Path getAddressBookFilePath() { - return addressBookStorage.getAddressBookFilePath(); - } - - @Override - public Optional readAddressBook() throws DataConversionException, IOException { - return readAddressBook(addressBookStorage.getAddressBookFilePath()); - } - - @Override - public Optional readAddressBook(Path filePath) throws DataConversionException, IOException { - logger.fine("Attempting to read data from file: " + filePath); - return addressBookStorage.readAddressBook(filePath); - } - - @Override - public void saveAddressBook(ReadOnlyAddressBook addressBook) throws IOException { - saveAddressBook(addressBook, addressBookStorage.getAddressBookFilePath()); - } - - @Override - public void saveAddressBook(ReadOnlyAddressBook addressBook, Path filePath) throws IOException { - logger.fine("Attempting to write to data file: " + filePath); - addressBookStorage.saveAddressBook(addressBook, filePath); - } - -} diff --git a/src/main/java/seedu/address/ui/CommandBox.java b/src/main/java/seedu/address/ui/CommandBox.java deleted file mode 100644 index 9e75478664b..00000000000 --- a/src/main/java/seedu/address/ui/CommandBox.java +++ /dev/null @@ -1,85 +0,0 @@ -package seedu.address.ui; - -import javafx.collections.ObservableList; -import javafx.fxml.FXML; -import javafx.scene.control.TextField; -import javafx.scene.layout.Region; -import seedu.address.logic.commands.CommandResult; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.logic.parser.exceptions.ParseException; - -/** - * The UI component that is responsible for receiving user command inputs. - */ -public class CommandBox extends UiPart { - - public static final String ERROR_STYLE_CLASS = "error"; - private static final String FXML = "CommandBox.fxml"; - - private final CommandExecutor commandExecutor; - - @FXML - private TextField commandTextField; - - /** - * Creates a {@code CommandBox} with the given {@code CommandExecutor}. - */ - public CommandBox(CommandExecutor commandExecutor) { - super(FXML); - this.commandExecutor = commandExecutor; - // calls #setStyleToDefault() whenever there is a change to the text of the command box. - commandTextField.textProperty().addListener((unused1, unused2, unused3) -> setStyleToDefault()); - } - - /** - * Handles the Enter button pressed event. - */ - @FXML - private void handleCommandEntered() { - String commandText = commandTextField.getText(); - if (commandText.equals("")) { - return; - } - - try { - commandExecutor.execute(commandText); - commandTextField.setText(""); - } catch (CommandException | ParseException e) { - setStyleToIndicateCommandFailure(); - } - } - - /** - * Sets the command box style to use the default style. - */ - private void setStyleToDefault() { - commandTextField.getStyleClass().remove(ERROR_STYLE_CLASS); - } - - /** - * Sets the command box style to indicate a failed command. - */ - private void setStyleToIndicateCommandFailure() { - ObservableList styleClass = commandTextField.getStyleClass(); - - if (styleClass.contains(ERROR_STYLE_CLASS)) { - return; - } - - styleClass.add(ERROR_STYLE_CLASS); - } - - /** - * Represents a function that can execute commands. - */ - @FunctionalInterface - public interface CommandExecutor { - /** - * Executes the command and returns the result. - * - * @see seedu.address.logic.Logic#execute(String) - */ - CommandResult execute(String commandText) throws CommandException, ParseException; - } - -} diff --git a/src/main/java/seedu/address/ui/HelpWindow.java b/src/main/java/seedu/address/ui/HelpWindow.java deleted file mode 100644 index 3f16b2fcf26..00000000000 --- a/src/main/java/seedu/address/ui/HelpWindow.java +++ /dev/null @@ -1,102 +0,0 @@ -package seedu.address.ui; - -import java.util.logging.Logger; - -import javafx.fxml.FXML; -import javafx.scene.control.Button; -import javafx.scene.control.Label; -import javafx.scene.input.Clipboard; -import javafx.scene.input.ClipboardContent; -import javafx.stage.Stage; -import seedu.address.commons.core.LogsCenter; - -/** - * Controller for a help page - */ -public class HelpWindow extends UiPart { - - public static final String USERGUIDE_URL = "https://se-education.org/addressbook-level3/UserGuide.html"; - public static final String HELP_MESSAGE = "Refer to the user guide: " + USERGUIDE_URL; - - private static final Logger logger = LogsCenter.getLogger(HelpWindow.class); - private static final String FXML = "HelpWindow.fxml"; - - @FXML - private Button copyButton; - - @FXML - private Label helpMessage; - - /** - * Creates a new HelpWindow. - * - * @param root Stage to use as the root of the HelpWindow. - */ - public HelpWindow(Stage root) { - super(FXML, root); - helpMessage.setText(HELP_MESSAGE); - } - - /** - * Creates a new HelpWindow. - */ - public HelpWindow() { - this(new Stage()); - } - - /** - * Shows the help window. - * @throws IllegalStateException - *
    - *
  • - * if this method is called on a thread other than the JavaFX Application Thread. - *
  • - *
  • - * if this method is called during animation or layout processing. - *
  • - *
  • - * if this method is called on the primary stage. - *
  • - *
  • - * if {@code dialogStage} is already showing. - *
  • - *
- */ - public void show() { - logger.fine("Showing help page about the application."); - getRoot().show(); - getRoot().centerOnScreen(); - } - - /** - * Returns true if the help window is currently being shown. - */ - public boolean isShowing() { - return getRoot().isShowing(); - } - - /** - * Hides the help window. - */ - public void hide() { - getRoot().hide(); - } - - /** - * Focuses on the help window. - */ - public void focus() { - getRoot().requestFocus(); - } - - /** - * Copies the URL to the user guide to the clipboard. - */ - @FXML - private void copyUrl() { - final Clipboard clipboard = Clipboard.getSystemClipboard(); - final ClipboardContent url = new ClipboardContent(); - url.putString(USERGUIDE_URL); - clipboard.setContent(url); - } -} diff --git a/src/main/java/seedu/address/ui/PersonCard.java b/src/main/java/seedu/address/ui/PersonCard.java deleted file mode 100644 index 7fc927bc5d9..00000000000 --- a/src/main/java/seedu/address/ui/PersonCard.java +++ /dev/null @@ -1,77 +0,0 @@ -package seedu.address.ui; - -import java.util.Comparator; - -import javafx.fxml.FXML; -import javafx.scene.control.Label; -import javafx.scene.layout.FlowPane; -import javafx.scene.layout.HBox; -import javafx.scene.layout.Region; -import seedu.address.model.person.Person; - -/** - * An UI component that displays information of a {@code Person}. - */ -public class PersonCard extends UiPart { - - private static final String FXML = "PersonListCard.fxml"; - - /** - * Note: Certain keywords such as "location" and "resources" are reserved keywords in JavaFX. - * As a consequence, UI elements' variable names cannot be set to such keywords - * or an exception will be thrown by JavaFX during runtime. - * - * @see The issue on AddressBook level 4 - */ - - public final Person person; - - @FXML - private HBox cardPane; - @FXML - private Label name; - @FXML - private Label id; - @FXML - private Label phone; - @FXML - private Label address; - @FXML - private Label email; - @FXML - private FlowPane tags; - - /** - * Creates a {@code PersonCode} with the given {@code Person} and index to display. - */ - public PersonCard(Person person, int displayedIndex) { - super(FXML); - this.person = person; - id.setText(displayedIndex + ". "); - name.setText(person.getName().fullName); - phone.setText(person.getPhone().value); - address.setText(person.getAddress().value); - email.setText(person.getEmail().value); - person.getTags().stream() - .sorted(Comparator.comparing(tag -> tag.tagName)) - .forEach(tag -> tags.getChildren().add(new Label(tag.tagName))); - } - - @Override - public boolean equals(Object other) { - // short circuit if same object - if (other == this) { - return true; - } - - // instanceof handles nulls - if (!(other instanceof PersonCard)) { - return false; - } - - // state check - PersonCard card = (PersonCard) other; - return id.getText().equals(card.id.getText()) - && person.equals(card.person); - } -} diff --git a/src/main/java/seedu/address/ui/PersonListPanel.java b/src/main/java/seedu/address/ui/PersonListPanel.java deleted file mode 100644 index f4c501a897b..00000000000 --- a/src/main/java/seedu/address/ui/PersonListPanel.java +++ /dev/null @@ -1,49 +0,0 @@ -package seedu.address.ui; - -import java.util.logging.Logger; - -import javafx.collections.ObservableList; -import javafx.fxml.FXML; -import javafx.scene.control.ListCell; -import javafx.scene.control.ListView; -import javafx.scene.layout.Region; -import seedu.address.commons.core.LogsCenter; -import seedu.address.model.person.Person; - -/** - * Panel containing the list of persons. - */ -public class PersonListPanel extends UiPart { - private static final String FXML = "PersonListPanel.fxml"; - private final Logger logger = LogsCenter.getLogger(PersonListPanel.class); - - @FXML - private ListView personListView; - - /** - * Creates a {@code PersonListPanel} with the given {@code ObservableList}. - */ - public PersonListPanel(ObservableList personList) { - super(FXML); - personListView.setItems(personList); - personListView.setCellFactory(listView -> new PersonListViewCell()); - } - - /** - * Custom {@code ListCell} that displays the graphics of a {@code Person} using a {@code PersonCard}. - */ - class PersonListViewCell extends ListCell { - @Override - protected void updateItem(Person person, boolean empty) { - super.updateItem(person, empty); - - if (empty || person == null) { - setGraphic(null); - setText(null); - } else { - setGraphic(new PersonCard(person, getIndex() + 1).getRoot()); - } - } - } - -} diff --git a/src/main/resources/fonts/Inconsolata/Inconsolata-Regular.ttf b/src/main/resources/fonts/Inconsolata/Inconsolata-Regular.ttf new file mode 100644 index 00000000000..0d879bf3a47 Binary files /dev/null and b/src/main/resources/fonts/Inconsolata/Inconsolata-Regular.ttf differ diff --git a/src/main/resources/fonts/Inter/Inter-Black.ttf b/src/main/resources/fonts/Inter/Inter-Black.ttf new file mode 100644 index 00000000000..5aecf7dc414 Binary files /dev/null and b/src/main/resources/fonts/Inter/Inter-Black.ttf differ diff --git a/src/main/resources/fonts/Inter/Inter-Bold.ttf b/src/main/resources/fonts/Inter/Inter-Bold.ttf new file mode 100644 index 00000000000..8e82c70d108 Binary files /dev/null and b/src/main/resources/fonts/Inter/Inter-Bold.ttf differ diff --git a/src/main/resources/fonts/Inter/Inter-ExtraBold.ttf b/src/main/resources/fonts/Inter/Inter-ExtraBold.ttf new file mode 100644 index 00000000000..cb4b8217fc2 Binary files /dev/null and b/src/main/resources/fonts/Inter/Inter-ExtraBold.ttf differ diff --git a/src/main/resources/fonts/Inter/Inter-ExtraLight.ttf b/src/main/resources/fonts/Inter/Inter-ExtraLight.ttf new file mode 100644 index 00000000000..64aee30a4e0 Binary files /dev/null and b/src/main/resources/fonts/Inter/Inter-ExtraLight.ttf differ diff --git a/src/main/resources/fonts/Inter/Inter-Light.ttf b/src/main/resources/fonts/Inter/Inter-Light.ttf new file mode 100644 index 00000000000..9e265d8905d Binary files /dev/null and b/src/main/resources/fonts/Inter/Inter-Light.ttf differ diff --git a/src/main/resources/fonts/Inter/Inter-Medium.ttf b/src/main/resources/fonts/Inter/Inter-Medium.ttf new file mode 100644 index 00000000000..b53fb1c4acb Binary files /dev/null and b/src/main/resources/fonts/Inter/Inter-Medium.ttf differ diff --git a/src/main/resources/fonts/Inter/Inter-Regular.ttf b/src/main/resources/fonts/Inter/Inter-Regular.ttf new file mode 100644 index 00000000000..8d4eebf2066 Binary files /dev/null and b/src/main/resources/fonts/Inter/Inter-Regular.ttf differ diff --git a/src/main/resources/fonts/Inter/Inter-SemiBold.ttf b/src/main/resources/fonts/Inter/Inter-SemiBold.ttf new file mode 100644 index 00000000000..c6aeeb16a6d Binary files /dev/null and b/src/main/resources/fonts/Inter/Inter-SemiBold.ttf differ diff --git a/src/main/resources/fonts/Inter/Inter-Thin.ttf b/src/main/resources/fonts/Inter/Inter-Thin.ttf new file mode 100644 index 00000000000..7aed55d5600 Binary files /dev/null and b/src/main/resources/fonts/Inter/Inter-Thin.ttf differ diff --git a/src/main/resources/images/fasttrack_logo.png b/src/main/resources/images/fasttrack_logo.png new file mode 100644 index 00000000000..5b1d7175f8d Binary files /dev/null and b/src/main/resources/images/fasttrack_logo.png differ diff --git a/src/main/resources/view/CategoryListCard.fxml b/src/main/resources/view/CategoryListCard.fxml new file mode 100644 index 00000000000..a032735a2fc --- /dev/null +++ b/src/main/resources/view/CategoryListCard.fxml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/CategoryListPanel.fxml b/src/main/resources/view/CategoryListPanel.fxml new file mode 100644 index 00000000000..7b3add7d314 --- /dev/null +++ b/src/main/resources/view/CategoryListPanel.fxml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/main/resources/view/CommandBox.fxml b/src/main/resources/view/CommandBox.fxml index 09f6d6fe9e4..e48d813bcd9 100644 --- a/src/main/resources/view/CommandBox.fxml +++ b/src/main/resources/view/CommandBox.fxml @@ -1,9 +1,15 @@ + - - + + + + + + + + - diff --git a/src/main/resources/view/DarkTheme.css b/src/main/resources/view/DarkTheme.css index 36e6b001cd8..d63c4bb3aba 100644 --- a/src/main/resources/view/DarkTheme.css +++ b/src/main/resources/view/DarkTheme.css @@ -1,34 +1,35 @@ -.background { - -fx-background-color: derive(#1d1d1d, 20%); - background-color: #383838; /* Used in the default.html file */ +@font-face { + font-family: 'Inter Light'; + src: url('../fonts/Inter/Inter-Light.ttf'); } -.label { - -fx-font-size: 11pt; - -fx-font-family: "Segoe UI Semibold"; - -fx-text-fill: #555555; - -fx-opacity: 0.9; +@font-face { + font-family: 'Inter Regular'; + src: url('../fonts/Inter/Inter-Regular.ttf'); } -.label-bright { - -fx-font-size: 11pt; - -fx-font-family: "Segoe UI Semibold"; - -fx-text-fill: white; - -fx-opacity: 1; +@font-face { + font-family: 'Inter Bold'; + src: url('../fonts/Inter/Inter-Bold.ttf'); } -.label-header { - -fx-font-size: 32pt; - -fx-font-family: "Segoe UI Light"; - -fx-text-fill: white; - -fx-opacity: 1; +@font-face { + font-family: 'Inter ExtraBold'; + src: url('../fonts/Inter/Inter-ExtraBold.ttf'); } -.text-field { - -fx-font-size: 12pt; - -fx-font-family: "Segoe UI Semibold"; +@font-face { + font-family: 'Inconsolata'; + src: url('../fonts/Inconsolata/Inconsolata-Regular.ttf'); +} + + +.background { + -fx-background-color: #000026; + background-color: #000026; /* Used in the default.html file */ } + .tab-pane { -fx-padding: 0 0 0 1; } @@ -76,85 +77,208 @@ -fx-background-color: -fx-focus-color; } -.split-pane:horizontal .split-pane-divider { - -fx-background-color: derive(#1d1d1d, 20%); - -fx-border-color: transparent transparent transparent #4d4d4d; -} - -.split-pane { - -fx-border-radius: 1; - -fx-border-width: 1; - -fx-background-color: derive(#1d1d1d, 20%); +.split-pane:horizontal > .split-pane-divider { + -fx-divider-position: 0; + -fx-divider-width: 0; + -fx-background-color: transparent; } .list-view { - -fx-background-insets: 0; -fx-padding: 0; - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-color: #000026; } +.list-view .scroll-bar { + -fx-background-color: #000026; +} + +.list-view .scroll-bar:vertical .thumb { + -fx-background-color: #2E2E57; +} + +.list-view .scroll-bar:vertical { + -fx-pref-width: 10px; +} + +.text-area .scroll-bar:vertical { + -fx-background-color: #0A0B39; + -fx-pref-width: 8px; +} + +.text-area .scroll-bar:horizontal { + -fx-background-color: #0A0B39; + -fx-pref-height: 8px; +} + +.text-area .filler { + -fx-background-color: #0A0B39; + -fx-border-color: #0A0B39; +} + +.text-area .corner { + -fx-background-color: #0A0B39; + -fx-border-color: #0A0B39; +} + + +.text-area .scroll-bar:vertical .thumb { + -fx-background-color: #2E2E57; +} + +.text-area .scroll-bar:horizontal .thumb { + -fx-background-color: #2E2E57; +} + + .list-cell { -fx-label-padding: 0 0 0 0; -fx-graphic-text-gap : 0; + -fx-background-color: transparent; -fx-padding: 0 0 0 0; } -.list-cell:filled:even { - -fx-background-color: #3c3e3f; +.list-cell:filled:selected { + -fx-background-color: #0A0B39; +} + +.results_header { + -fx-font-family: "Inter Bold"; + -fx-font-size: 26px; + -fx-text-fill: white; } -.list-cell:filled:odd { - -fx-background-color: #515658; +.stats_header { + -fx-font-family: "Inter Regular"; + -fx-font-size: 12px; + -fx-text-fill: #606DA0; } -.list-cell:filled:selected { - -fx-background-color: #424d5f; +.stats_advice { + -fx-font-family: "Inter Bold"; + -fx-font-size: 16px; + -fx-text-fill: #606DA0; +} + +.stat_box { + -fx-background-color: #0A0B39; + -fx-background-radius: 8px; } -.list-cell:filled:selected #cardPane { - -fx-border-color: #3e7b91; - -fx-border-width: 1; +.change_indicator { + -fx-font-family: "Inter Regular"; + -fx-font-size: 12px; } -.list-cell .label { +.change_indicator.positive_change_indicator { + -fx-text-fill: #00FFB2; + -fx-background-color: #003C40; +} + +.change_indicator.negative_change_indicator { + -fx-text-fill: #FF0000; + -fx-background-color: #34001D; +} + +.change_indicator_background_positive { + -fx-background-color: #003C40; + -fx-background-radius: 6px; + -fx-padding: 10 12 10 12 +} + +.change_indicator_background_negative { + -fx-background-color: #34001D; + -fx-background-radius: 6px; + -fx-padding: 10 12 10 12 +} + + +.filter_name { + -fx-text-fill: #606DA0; +} + +.details_header { + -fx-font-family: "Inter Regular"; + -fx-font-size: 14px; + -fx-text-fill: #606DA0; +} + +.statDetail { + -fx-font-family: "Inter Bold"; + -fx-font-size: 20px; + -fx-text-fill: white; +} + +.results_count { + -fx-text-fill: #0F51F0; +} + +.date_filter { + -fx-text-fill: #0F51F0; +} + + + +.cell_label { + -fx-font-family: "Inter Regular"; + -fx-font-size: 14px; -fx-text-fill: white; } -.cell_big_label { - -fx-font-family: "Segoe UI Semibold"; - -fx-font-size: 16px; - -fx-text-fill: #010504; +.label_dark { + -fx-text-fill: #606DA0; +} + + +.category_tag { + -fx-font-family: "Inter Regular"; + -fx-font-size: 10px; + -fx-background-color: #0B134F; + -fx-background-radius: 6px; + -fx-text-fill: #0F51F0; + -fx-padding: 8 12 8 12 +} + +.suggestionPane { + -fx-background-color: #000032; + -fx-border-style: none; +} + +.suggestionPane .list-cell:filled:selected { + -fx-background-color: #0B134F; + -fx-border-style: none; + -fx-background-radius: 8px; } -.cell_small_label { - -fx-font-family: "Segoe UI"; - -fx-font-size: 13px; - -fx-text-fill: #010504; +.suggestionPane .scroll-bar { + -fx-background-color: #000032; } + .stack-pane { - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-color: #000026 } -.pane-with-border { - -fx-background-color: derive(#1d1d1d, 20%); - -fx-border-color: derive(#1d1d1d, 10%); - -fx-border-top-width: 1px; +.pane { + -fx-background-color: #000026; } .status-bar { - -fx-background-color: derive(#1d1d1d, 30%); + -fx-background-color: #000026; } .result-display { - -fx-background-color: transparent; - -fx-font-family: "Segoe UI Light"; - -fx-font-size: 13pt; + -fx-background-color: #0A0B39; + -fx-font-family: "Inconsolata"; + -fx-font-size: 12pt; -fx-text-fill: white; + -fx-border-color: #0F51F0; + -fx-border-width: 0 0 0 3px; } + .result-display .label { -fx-text-fill: black !important; + -fx-background-color: #0A0B39; } .status-bar .label { @@ -181,11 +305,11 @@ } .grid-pane .stack-pane { - -fx-background-color: derive(#1d1d1d, 30%); + -fx-background-color: #000026; } .context-menu { - -fx-background-color: derive(#1d1d1d, 50%); + -fx-background-color: #000026; } .context-menu .label { @@ -193,13 +317,13 @@ } .menu-bar { - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-color: #000026; } .menu-bar .label { - -fx-font-size: 14pt; - -fx-font-family: "Segoe UI Light"; - -fx-text-fill: white; + -fx-font-size: 12pt; + -fx-font-family: "Inter Regular"; + -fx-text-fill: #606DA0; -fx-opacity: 0.9; } @@ -207,6 +331,16 @@ -fx-background-color: black; } +.menu-bar { + -fx-selection-bar: #0A0B39 ; +} + + + +.menu-item:hover, .menu:hover { + -fx-background-color: #0A0B39; +} + /* * Metro style Push Button * Author: Pedro Duque Vieira @@ -281,14 +415,6 @@ -fx-text-fill: white; } -.scroll-bar { - -fx-background-color: derive(#1d1d1d, 20%); -} - -.scroll-bar .thumb { - -fx-background-color: derive(#1d1d1d, 50%); - -fx-background-insets: 3; -} .scroll-bar .increment-button, .scroll-bar .decrement-button { -fx-background-color: transparent; @@ -313,19 +439,19 @@ } #commandTypeLabel { - -fx-font-size: 11px; - -fx-text-fill: #F70D1A; + -fx-font-size: 12px; + -fx-text-fill: #606DA0; } #commandTextField { - -fx-background-color: transparent #383838 transparent #383838; + -fx-background-color: #0A0B39; -fx-background-insets: 0; - -fx-border-color: #383838 #383838 #ffffff #383838; - -fx-border-insets: 0; - -fx-border-width: 1; - -fx-font-family: "Segoe UI Light"; - -fx-font-size: 13pt; + -fx-font-family: "Inter Regular"; + -fx-font-size: 12pt; -fx-text-fill: white; + -fx-prompt-text-fill: #606DA0; + -fx-border-color: #0F51F0; + -fx-border-width: 0 0 3 0px; } #filterField, #personListPanel, #personWebpage { @@ -333,7 +459,7 @@ } #resultDisplay .content { - -fx-background-color: transparent, #383838, transparent, #383838; + -fx-background-color: #0A0B39; -fx-background-radius: 0; } diff --git a/src/main/resources/view/ExpenseListCard.fxml b/src/main/resources/view/ExpenseListCard.fxml new file mode 100644 index 00000000000..eaace2f966c --- /dev/null +++ b/src/main/resources/view/ExpenseListCard.fxml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/ExpenseListPanel.fxml b/src/main/resources/view/ExpenseListPanel.fxml new file mode 100644 index 00000000000..a5d65af7817 --- /dev/null +++ b/src/main/resources/view/ExpenseListPanel.fxml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/main/resources/view/Extensions.css b/src/main/resources/view/Extensions.css index bfe82a85964..9dab766e185 100644 --- a/src/main/resources/view/Extensions.css +++ b/src/main/resources/view/Extensions.css @@ -16,5 +16,5 @@ } .tooltip-text { - -fx-text-fill: white; + -fx-text-fill: #606DA0; } diff --git a/src/main/resources/view/MainWindow.fxml b/src/main/resources/view/MainWindow.fxml index a431648f6c0..7ed580c06ad 100644 --- a/src/main/resources/view/MainWindow.fxml +++ b/src/main/resources/view/MainWindow.fxml @@ -3,57 +3,107 @@ + + + - + - + - + - - + + + + + - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + - + - + - - + + + + - + + + + + diff --git a/src/main/resources/view/PersonListCard.fxml b/src/main/resources/view/PersonListCard.fxml deleted file mode 100644 index f08ea32ad55..00000000000 --- a/src/main/resources/view/PersonListCard.fxml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/main/resources/view/PersonListPanel.fxml b/src/main/resources/view/PersonListPanel.fxml deleted file mode 100644 index 8836d323cc5..00000000000 --- a/src/main/resources/view/PersonListPanel.fxml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/src/main/resources/view/RecurringExpenseListCard.fxml b/src/main/resources/view/RecurringExpenseListCard.fxml new file mode 100644 index 00000000000..c786f75b953 --- /dev/null +++ b/src/main/resources/view/RecurringExpenseListCard.fxml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/RecurringExpenseListPanel.fxml b/src/main/resources/view/RecurringExpenseListPanel.fxml new file mode 100644 index 00000000000..2f9f2db3a77 --- /dev/null +++ b/src/main/resources/view/RecurringExpenseListPanel.fxml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/main/resources/view/ResultDisplay.fxml b/src/main/resources/view/ResultDisplay.fxml index 58d5ad3dc56..37f4999c760 100644 --- a/src/main/resources/view/ResultDisplay.fxml +++ b/src/main/resources/view/ResultDisplay.fxml @@ -1,9 +1,21 @@ + - - + + + diff --git a/src/main/resources/view/ResultsDetails.fxml b/src/main/resources/view/ResultsDetails.fxml new file mode 100644 index 00000000000..74f9f895269 --- /dev/null +++ b/src/main/resources/view/ResultsDetails.fxml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/ResultsHeader.fxml b/src/main/resources/view/ResultsHeader.fxml new file mode 100644 index 00000000000..2f36fdc9b74 --- /dev/null +++ b/src/main/resources/view/ResultsHeader.fxml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/StatisticsPanel.fxml b/src/main/resources/view/StatisticsPanel.fxml new file mode 100644 index 00000000000..0d56b886117 --- /dev/null +++ b/src/main/resources/view/StatisticsPanel.fxml @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/StatusBarFooter.fxml b/src/main/resources/view/StatusBarFooter.fxml index 149f62bd29c..04d859dd9be 100644 --- a/src/main/resources/view/StatusBarFooter.fxml +++ b/src/main/resources/view/StatusBarFooter.fxml @@ -1,12 +1,20 @@ + + - + diff --git a/src/main/resources/view/SuggestionListCard.fxml b/src/main/resources/view/SuggestionListCard.fxml new file mode 100644 index 00000000000..f01825b0418 --- /dev/null +++ b/src/main/resources/view/SuggestionListCard.fxml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/SuggestionListPanel.fxml b/src/main/resources/view/SuggestionListPanel.fxml new file mode 100644 index 00000000000..2664813bea5 --- /dev/null +++ b/src/main/resources/view/SuggestionListPanel.fxml @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/test/data/JsonAddressBookStorageTest/notJsonFormatAddressBook.json b/src/test/data/JsonAddressBookStorageTest/notJsonFormatExpenseTracker.json similarity index 100% rename from src/test/data/JsonAddressBookStorageTest/notJsonFormatAddressBook.json rename to src/test/data/JsonAddressBookStorageTest/notJsonFormatExpenseTracker.json diff --git a/src/test/data/JsonUserPrefsStorageTest/ExtraValuesUserPref.json b/src/test/data/JsonUserPrefsStorageTest/ExtraValuesUserPref.json index 1037548a9cd..daff9e124a8 100644 --- a/src/test/data/JsonUserPrefsStorageTest/ExtraValuesUserPref.json +++ b/src/test/data/JsonUserPrefsStorageTest/ExtraValuesUserPref.json @@ -9,5 +9,5 @@ "z" : 99 } }, - "addressBookFilePath" : "addressbook.json" + "expenseTrackerFilePath" : "data/fastTrack.json" } diff --git a/src/test/data/JsonUserPrefsStorageTest/TypicalUserPref.json b/src/test/data/JsonUserPrefsStorageTest/TypicalUserPref.json index b819bed900a..7eea82636f5 100644 --- a/src/test/data/JsonUserPrefsStorageTest/TypicalUserPref.json +++ b/src/test/data/JsonUserPrefsStorageTest/TypicalUserPref.json @@ -7,5 +7,6 @@ "y" : 100 } }, - "addressBookFilePath" : "addressbook.json" + "expenseTrackerFilePath" : "data/fastTrack.json" } + diff --git a/src/test/java/seedu/address/AppParametersTest.java b/src/test/java/fasttrack/AppParametersTest.java similarity index 98% rename from src/test/java/seedu/address/AppParametersTest.java rename to src/test/java/fasttrack/AppParametersTest.java index 61326b2d31a..851d9be0c96 100644 --- a/src/test/java/seedu/address/AppParametersTest.java +++ b/src/test/java/fasttrack/AppParametersTest.java @@ -1,4 +1,4 @@ -package seedu.address; +package fasttrack; import static org.junit.jupiter.api.Assertions.assertEquals; diff --git a/src/test/java/seedu/address/commons/core/ConfigTest.java b/src/test/java/fasttrack/commons/core/ConfigTest.java similarity index 95% rename from src/test/java/seedu/address/commons/core/ConfigTest.java rename to src/test/java/fasttrack/commons/core/ConfigTest.java index 07cd7f73d53..64d17370edc 100644 --- a/src/test/java/seedu/address/commons/core/ConfigTest.java +++ b/src/test/java/fasttrack/commons/core/ConfigTest.java @@ -1,4 +1,4 @@ -package seedu.address.commons.core; +package fasttrack.commons.core; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; diff --git a/src/test/java/seedu/address/commons/core/VersionTest.java b/src/test/java/fasttrack/commons/core/VersionTest.java similarity index 98% rename from src/test/java/seedu/address/commons/core/VersionTest.java rename to src/test/java/fasttrack/commons/core/VersionTest.java index 495cd231554..42a2c4b83b1 100644 --- a/src/test/java/seedu/address/commons/core/VersionTest.java +++ b/src/test/java/fasttrack/commons/core/VersionTest.java @@ -1,8 +1,8 @@ -package seedu.address.commons.core; +package fasttrack.commons.core; +import static fasttrack.testutil.Assert.assertThrows; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -import static seedu.address.testutil.Assert.assertThrows; import org.junit.jupiter.api.Test; diff --git a/src/test/java/seedu/address/commons/core/index/IndexTest.java b/src/test/java/fasttrack/commons/core/index/IndexTest.java similarity index 95% rename from src/test/java/seedu/address/commons/core/index/IndexTest.java rename to src/test/java/fasttrack/commons/core/index/IndexTest.java index a3ec6f8e747..a8b547f9353 100644 --- a/src/test/java/seedu/address/commons/core/index/IndexTest.java +++ b/src/test/java/fasttrack/commons/core/index/IndexTest.java @@ -1,9 +1,9 @@ -package seedu.address.commons.core.index; +package fasttrack.commons.core.index; +import static fasttrack.testutil.Assert.assertThrows; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; -import static seedu.address.testutil.Assert.assertThrows; import org.junit.jupiter.api.Test; diff --git a/src/test/java/fasttrack/commons/stubs/ModelStub.java b/src/test/java/fasttrack/commons/stubs/ModelStub.java new file mode 100644 index 00000000000..6ca08c49022 --- /dev/null +++ b/src/test/java/fasttrack/commons/stubs/ModelStub.java @@ -0,0 +1,228 @@ +package fasttrack.commons.stubs; + +import static java.util.Objects.requireNonNull; + +import java.nio.file.Path; +import java.util.function.Predicate; + +import fasttrack.commons.core.GuiSettings; +import fasttrack.logic.parser.ParserUtil; +import fasttrack.model.Budget; +import fasttrack.model.Model; +import fasttrack.model.ReadOnlyExpenseTracker; +import fasttrack.model.ReadOnlyUserPrefs; +import fasttrack.model.category.Category; +import fasttrack.model.category.MiscellaneousCategory; +import fasttrack.model.expense.Expense; +import fasttrack.model.expense.RecurringExpenseManager; +import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +/** + * A default model stub that have some methods failing. + */ +public class ModelStub implements Model { + private ObservableList categories = FXCollections.observableArrayList(); + private ObservableList expenses = FXCollections.observableArrayList(); + private ObservableList recurringGenerators = FXCollections.observableArrayList(); + + @Override + public void addCategory(Category toAdd) { + requireNonNull(toAdd); + categories.add(toAdd); + } + + @Override + public void addExpense(Expense toAdd) { + requireNonNull(toAdd); + expenses.add(toAdd); + } + + // All other methods should throw an AssertionError + @Override + public void setUserPrefs(ReadOnlyUserPrefs userPrefs) { + throw new AssertionError("This method should not be called."); + } + + @Override + public ReadOnlyUserPrefs getUserPrefs() { + throw new AssertionError("This method should not be called."); + } + + @Override + public GuiSettings getGuiSettings() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setGuiSettings(GuiSettings guiSettings) { + throw new AssertionError("This method should not be called."); + } + + @Override + public Path getExpenseTrackerFilePath() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setExpenseTrackerFilePath(Path expenseTrackerFilePath) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setExpenseTracker(ReadOnlyExpenseTracker expenseTracker) { + throw new AssertionError("This method should not be called."); + } + + @Override + public ReadOnlyExpenseTracker getExpenseTracker() { + throw new AssertionError("This method should not be called."); + } + + @Override + public SimpleObjectProperty getAppliedTimeSpanFilter() { + return null; + } + + @Override + public SimpleObjectProperty getAppliedCategoryFilter() { + return null; + } + + @Override + public void updateTimeSpanFilter(ParserUtil.Timespan timeSpan) { + + } + + @Override + public void updateCategoryFilter(Category category) { + + } + + + @Override + public void deleteExpense(Expense expense) { + // Delete the expense from the list + expenses.remove(expense); + } + + @Override + public void clearExpense() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setExpense(Expense target, Expense editedExpense) { + // set the expense in the list + int index = expenses.indexOf(target); + expenses.set(index, editedExpense); + } + + @Override + public void setExpense(int index, Expense newExpense) { + throw new AssertionError("This method should not be called."); + } + + @Override + public boolean hasExpense(Expense expense) { + // Check if the expense is in the list + return expenses.contains(expense); + } + + @Override + public void updateFilteredExpensesList(Predicate predicate) { + // update the list + expenses.removeIf(predicate); + } + + @Override + public ObservableList getFilteredExpenseList() { + // get the filtered list + return expenses; + } + + @Override + public void deleteCategory(Category target) { + // Delete the category from the list + categories.remove(target); + // change the category of the expenses in the list + for (Expense expense : expenses) { + if (expense.getCategory().equals(target)) { + expense.setCategory(new MiscellaneousCategory()); + } + } + } + + @Override + public void clearCategory() { + throw new AssertionError("This method should not be called."); + } + + @Override + public boolean hasCategory(Category category) { + // Check if the category is in the list + return categories.contains(category); + } + + @Override + public ObservableList getFilteredCategoryList() { + return categories; + } + + @Override + public void updateFilteredCategoryList(Predicate predicate) { + // update the list + categories.removeIf(predicate); + } + + @Override + public Category getCategoryInstance(Category categoryName) { + // get the category instance + return null; + } + + @Override + public void setBudget(Budget budget) { + throw new AssertionError("This method should not be called."); + } + + + public boolean hasRecurringExpense(RecurringExpenseManager recurringExpenseManager) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void addRecurringGenerator(RecurringExpenseManager recurringExpenseManager) { + throw new AssertionError("This method should not be called."); + } + + @Override + public ObservableList getRecurringExpenseGenerators() { + return recurringGenerators; + } + + @Override + public void updateFilteredRecurringGenerators(Predicate predicate) { + throw new AssertionError("This method should not be called."); + } + + /** + * Delete all recurring expense generators. + */ + @Override + public void clearRecurringExpenseGenerator() { + + } + + @Override + public void deleteRecurringExpense(RecurringExpenseManager recurringExpenseManager) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void addRetroactiveExpenses() { + throw new AssertionError("This method should not be called."); + } + +} diff --git a/src/test/java/seedu/address/commons/util/AppUtilTest.java b/src/test/java/fasttrack/commons/util/AppUtilTest.java similarity index 85% rename from src/test/java/seedu/address/commons/util/AppUtilTest.java rename to src/test/java/fasttrack/commons/util/AppUtilTest.java index 594de1e6365..ad761ece4ad 100644 --- a/src/test/java/seedu/address/commons/util/AppUtilTest.java +++ b/src/test/java/fasttrack/commons/util/AppUtilTest.java @@ -1,7 +1,7 @@ -package seedu.address.commons.util; +package fasttrack.commons.util; +import static fasttrack.testutil.Assert.assertThrows; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static seedu.address.testutil.Assert.assertThrows; import org.junit.jupiter.api.Test; @@ -9,7 +9,7 @@ public class AppUtilTest { @Test public void getImage_exitingImage() { - assertNotNull(AppUtil.getImage("/images/address_book_32.png")); + assertNotNull(AppUtil.getImage("/images/fasttrack_logo.png")); } @Test diff --git a/src/test/java/seedu/address/commons/util/CollectionUtilTest.java b/src/test/java/fasttrack/commons/util/CollectionUtilTest.java similarity index 96% rename from src/test/java/seedu/address/commons/util/CollectionUtilTest.java rename to src/test/java/fasttrack/commons/util/CollectionUtilTest.java index b467a3dc025..446338a6f05 100644 --- a/src/test/java/seedu/address/commons/util/CollectionUtilTest.java +++ b/src/test/java/fasttrack/commons/util/CollectionUtilTest.java @@ -1,9 +1,9 @@ -package seedu.address.commons.util; +package fasttrack.commons.util; +import static fasttrack.commons.util.CollectionUtil.requireAllNonNull; +import static fasttrack.testutil.Assert.assertThrows; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; -import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; -import static seedu.address.testutil.Assert.assertThrows; import java.util.Arrays; import java.util.Collection; diff --git a/src/test/java/seedu/address/commons/util/ConfigUtilTest.java b/src/test/java/fasttrack/commons/util/ConfigUtilTest.java similarity index 94% rename from src/test/java/seedu/address/commons/util/ConfigUtilTest.java rename to src/test/java/fasttrack/commons/util/ConfigUtilTest.java index d2ab2839a52..c8a4fc41114 100644 --- a/src/test/java/seedu/address/commons/util/ConfigUtilTest.java +++ b/src/test/java/fasttrack/commons/util/ConfigUtilTest.java @@ -1,8 +1,8 @@ -package seedu.address.commons.util; +package fasttrack.commons.util; +import static fasttrack.testutil.Assert.assertThrows; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; -import static seedu.address.testutil.Assert.assertThrows; import java.io.IOException; import java.nio.file.Path; @@ -13,8 +13,8 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; -import seedu.address.commons.core.Config; -import seedu.address.commons.exceptions.DataConversionException; +import fasttrack.commons.core.Config; +import fasttrack.commons.exceptions.DataConversionException; public class ConfigUtilTest { diff --git a/src/test/java/seedu/address/commons/util/FileUtilTest.java b/src/test/java/fasttrack/commons/util/FileUtilTest.java similarity index 84% rename from src/test/java/seedu/address/commons/util/FileUtilTest.java rename to src/test/java/fasttrack/commons/util/FileUtilTest.java index 1fe5478c756..e3688690ebd 100644 --- a/src/test/java/seedu/address/commons/util/FileUtilTest.java +++ b/src/test/java/fasttrack/commons/util/FileUtilTest.java @@ -1,8 +1,8 @@ -package seedu.address.commons.util; +package fasttrack.commons.util; +import static fasttrack.testutil.Assert.assertThrows; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; -import static seedu.address.testutil.Assert.assertThrows; import org.junit.jupiter.api.Test; diff --git a/src/test/java/seedu/address/commons/util/JsonUtilTest.java b/src/test/java/fasttrack/commons/util/JsonUtilTest.java similarity index 92% rename from src/test/java/seedu/address/commons/util/JsonUtilTest.java rename to src/test/java/fasttrack/commons/util/JsonUtilTest.java index d4907539dee..9d9936f0acd 100644 --- a/src/test/java/seedu/address/commons/util/JsonUtilTest.java +++ b/src/test/java/fasttrack/commons/util/JsonUtilTest.java @@ -1,4 +1,4 @@ -package seedu.address.commons.util; +package fasttrack.commons.util; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -7,8 +7,8 @@ import org.junit.jupiter.api.Test; -import seedu.address.testutil.SerializableTestClass; -import seedu.address.testutil.TestUtil; +import fasttrack.testutil.SerializableTestClass; +import fasttrack.testutil.TestUtil; /** * Tests JSON Read and Write diff --git a/src/test/java/seedu/address/commons/util/StringUtilTest.java b/src/test/java/fasttrack/commons/util/StringUtilTest.java similarity index 98% rename from src/test/java/seedu/address/commons/util/StringUtilTest.java rename to src/test/java/fasttrack/commons/util/StringUtilTest.java index c56d407bf3f..40f94aa3842 100644 --- a/src/test/java/seedu/address/commons/util/StringUtilTest.java +++ b/src/test/java/fasttrack/commons/util/StringUtilTest.java @@ -1,8 +1,8 @@ -package seedu.address.commons.util; +package fasttrack.commons.util; +import static fasttrack.testutil.Assert.assertThrows; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; -import static seedu.address.testutil.Assert.assertThrows; import java.io.FileNotFoundException; diff --git a/src/test/java/seedu/address/logic/LogicManagerTest.java b/src/test/java/fasttrack/logic/LogicManagerTest.java similarity index 54% rename from src/test/java/seedu/address/logic/LogicManagerTest.java rename to src/test/java/fasttrack/logic/LogicManagerTest.java index ad923ac249a..0827f855ddb 100644 --- a/src/test/java/seedu/address/logic/LogicManagerTest.java +++ b/src/test/java/fasttrack/logic/LogicManagerTest.java @@ -1,36 +1,32 @@ -package seedu.address.logic; +package fasttrack.logic; +import static fasttrack.commons.core.Messages.MESSAGE_INVALID_INDEX; +import static fasttrack.commons.core.Messages.MESSAGE_UNKNOWN_COMMAND; +import static fasttrack.testutil.Assert.assertThrows; import static org.junit.jupiter.api.Assertions.assertEquals; -import static seedu.address.commons.core.Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX; -import static seedu.address.commons.core.Messages.MESSAGE_UNKNOWN_COMMAND; -import static seedu.address.logic.commands.CommandTestUtil.ADDRESS_DESC_AMY; -import static seedu.address.logic.commands.CommandTestUtil.EMAIL_DESC_AMY; -import static seedu.address.logic.commands.CommandTestUtil.NAME_DESC_AMY; -import static seedu.address.logic.commands.CommandTestUtil.PHONE_DESC_AMY; -import static seedu.address.testutil.Assert.assertThrows; -import static seedu.address.testutil.TypicalPersons.AMY; import java.io.IOException; import java.nio.file.Path; +import java.time.LocalDate; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; -import seedu.address.logic.commands.AddCommand; -import seedu.address.logic.commands.CommandResult; -import seedu.address.logic.commands.ListCommand; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.Model; -import seedu.address.model.ModelManager; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.UserPrefs; -import seedu.address.model.person.Person; -import seedu.address.storage.JsonAddressBookStorage; -import seedu.address.storage.JsonUserPrefsStorage; -import seedu.address.storage.StorageManager; -import seedu.address.testutil.PersonBuilder; +import fasttrack.commons.core.Messages; +import fasttrack.logic.commands.CommandResult; +import fasttrack.logic.commands.exceptions.CommandException; +import fasttrack.logic.commands.list.ListExpensesCommand; +import fasttrack.logic.parser.exceptions.ParseException; +import fasttrack.model.Model; +import fasttrack.model.ModelManager; +import fasttrack.model.ReadOnlyExpenseTracker; +import fasttrack.model.UserPrefs; +import fasttrack.model.category.MiscellaneousCategory; +import fasttrack.model.expense.Expense; +import fasttrack.storage.JsonExpenseTrackerStorage; +import fasttrack.storage.JsonUserPrefsStorage; +import fasttrack.storage.StorageManager; public class LogicManagerTest { private static final IOException DUMMY_IO_EXCEPTION = new IOException("dummy exception"); @@ -38,16 +34,16 @@ public class LogicManagerTest { @TempDir public Path temporaryFolder; - private Model model = new ModelManager(); + private Model dataModel = new ModelManager(); private Logic logic; @BeforeEach public void setUp() { - JsonAddressBookStorage addressBookStorage = - new JsonAddressBookStorage(temporaryFolder.resolve("addressBook.json")); + JsonExpenseTrackerStorage addressBookStorage = new JsonExpenseTrackerStorage( + temporaryFolder.resolve("fastTrack.json")); JsonUserPrefsStorage userPrefsStorage = new JsonUserPrefsStorage(temporaryFolder.resolve("userPrefs.json")); StorageManager storage = new StorageManager(addressBookStorage, userPrefsStorage); - logic = new LogicManager(model, storage); + logic = new LogicManager(dataModel, storage); } @Test @@ -59,56 +55,65 @@ public void execute_invalidCommandFormat_throwsParseException() { @Test public void execute_commandExecutionError_throwsCommandException() { String deleteCommand = "delete 9"; - assertCommandException(deleteCommand, MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + assertCommandException(deleteCommand, MESSAGE_INVALID_INDEX); } @Test public void execute_validCommand_success() throws Exception { - String listCommand = ListCommand.COMMAND_WORD; - assertCommandSuccess(listCommand, ListCommand.MESSAGE_SUCCESS, model); + String listCommand = ListExpensesCommand.COMMAND_WORD; + assertCommandSuccess(listCommand, + String.format(Messages.MESSAGE_EXPENSES_LISTED_OVERVIEW, dataModel.getFilteredExpenseList().size()), + dataModel); } @Test public void execute_storageThrowsIoException_throwsCommandException() { // Setup LogicManager with JsonAddressBookIoExceptionThrowingStub - JsonAddressBookStorage addressBookStorage = - new JsonAddressBookIoExceptionThrowingStub(temporaryFolder.resolve("ioExceptionAddressBook.json")); - JsonUserPrefsStorage userPrefsStorage = - new JsonUserPrefsStorage(temporaryFolder.resolve("ioExceptionUserPrefs.json")); + JsonExpenseTrackerStorage addressBookStorage = new JsonAddressBookIoExceptionThrowingStub( + temporaryFolder.resolve("ioExceptionFastTrack.json")); + JsonUserPrefsStorage userPrefsStorage = new JsonUserPrefsStorage( + temporaryFolder.resolve("ioExceptionUserPrefs.json")); StorageManager storage = new StorageManager(addressBookStorage, userPrefsStorage); - logic = new LogicManager(model, storage); + logic = new LogicManager(dataModel, storage); // Execute add command - String addCommand = AddCommand.COMMAND_WORD + NAME_DESC_AMY + PHONE_DESC_AMY + EMAIL_DESC_AMY - + ADDRESS_DESC_AMY; - Person expectedPerson = new PersonBuilder(AMY).withTags().build(); + //String addCommand = AddCommand.COMMAND_WORD + NAME_DESC_AMY + PHONE_DESC_AMY + EMAIL_DESC_AMY + // + ADDRESS_DESC_AMY; + //Person expectedPerson = new PersonBuilder(AMY).withTags().build(); ModelManager expectedModel = new ModelManager(); - expectedModel.addPerson(expectedPerson); + expectedModel.addExpense(new Expense("apples", "4.5", LocalDate.now(), new MiscellaneousCategory())); String expectedMessage = LogicManager.FILE_OPS_ERROR_MESSAGE + DUMMY_IO_EXCEPTION; - assertCommandFailure(addCommand, CommandException.class, expectedMessage, expectedModel); + //assertCommandFailure(addCommand, CommandException.class, expectedMessage, expectedModel); } @Test - public void getFilteredPersonList_modifyList_throwsUnsupportedOperationException() { - assertThrows(UnsupportedOperationException.class, () -> logic.getFilteredPersonList().remove(0)); + public void getFilteredExpenseList_modifyList_throwsUnsupportedOperationException() { + assertThrows(UnsupportedOperationException.class, () -> logic.getFilteredExpenseList().remove(0)); + } + + @Test + public void getFilteredCategoryList_modifyList_throwsUnsupportedOperationException() { + assertThrows(UnsupportedOperationException.class, () -> logic.getFilteredCategoryList().remove(0)); } /** * Executes the command and confirms that * - no exceptions are thrown
* - the feedback message is equal to {@code expectedMessage}
- * - the internal model manager state is the same as that in {@code expectedModel}
+ * - the internal model manager state is the same as that in + * {@code expectedModel}
* @see #assertCommandFailure(String, Class, String, Model) */ private void assertCommandSuccess(String inputCommand, String expectedMessage, - Model expectedModel) throws CommandException, ParseException { + Model expectedDataModel) throws CommandException, ParseException { CommandResult result = logic.execute(inputCommand); assertEquals(expectedMessage, result.getFeedbackToUser()); - assertEquals(expectedModel, model); + assertEquals(expectedDataModel, dataModel); } /** - * Executes the command, confirms that a ParseException is thrown and that the result message is correct. + * Executes the command, confirms that a ParseException is thrown and that the + * result message is correct. * @see #assertCommandFailure(String, Class, String, Model) */ private void assertParseException(String inputCommand, String expectedMessage) { @@ -116,7 +121,8 @@ private void assertParseException(String inputCommand, String expectedMessage) { } /** - * Executes the command, confirms that a CommandException is thrown and that the result message is correct. + * Executes the command, confirms that a CommandException is thrown and that the + * result message is correct. * @see #assertCommandFailure(String, Class, String, Model) */ private void assertCommandException(String inputCommand, String expectedMessage) { @@ -124,38 +130,40 @@ private void assertCommandException(String inputCommand, String expectedMessage) } /** - * Executes the command, confirms that the exception is thrown and that the result message is correct. + * Executes the command, confirms that the exception is thrown and that the + * result message is correct. * @see #assertCommandFailure(String, Class, String, Model) */ private void assertCommandFailure(String inputCommand, Class expectedException, String expectedMessage) { - Model expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); - assertCommandFailure(inputCommand, expectedException, expectedMessage, expectedModel); + Model expectedDataModel = new ModelManager(dataModel.getExpenseTracker(), new UserPrefs()); + assertCommandFailure(inputCommand, expectedException, expectedMessage, expectedDataModel); } /** * Executes the command and confirms that * - the {@code expectedException} is thrown
* - the resulting error message is equal to {@code expectedMessage}
- * - the internal model manager state is the same as that in {@code expectedModel}
+ * - the internal model manager state is the same as that in + * {@code expectedModel}
* @see #assertCommandSuccess(String, String, Model) */ private void assertCommandFailure(String inputCommand, Class expectedException, - String expectedMessage, Model expectedModel) { + String expectedMessage, Model expectedDataModel) { assertThrows(expectedException, expectedMessage, () -> logic.execute(inputCommand)); - assertEquals(expectedModel, model); + assertEquals(expectedDataModel, dataModel); } /** * A stub class to throw an {@code IOException} when the save method is called. */ - private static class JsonAddressBookIoExceptionThrowingStub extends JsonAddressBookStorage { + private static class JsonAddressBookIoExceptionThrowingStub extends JsonExpenseTrackerStorage { private JsonAddressBookIoExceptionThrowingStub(Path filePath) { super(filePath); } @Override - public void saveAddressBook(ReadOnlyAddressBook addressBook, Path filePath) throws IOException { + public void saveExpenseTracker(ReadOnlyExpenseTracker addressBook, Path filePath) throws IOException { throw DUMMY_IO_EXCEPTION; } } diff --git a/src/test/java/seedu/address/logic/commands/CommandResultTest.java b/src/test/java/fasttrack/logic/commands/CommandResultTest.java similarity index 57% rename from src/test/java/seedu/address/logic/commands/CommandResultTest.java rename to src/test/java/fasttrack/logic/commands/CommandResultTest.java index 4f3eb46e9ef..ef560b15939 100644 --- a/src/test/java/seedu/address/logic/commands/CommandResultTest.java +++ b/src/test/java/fasttrack/logic/commands/CommandResultTest.java @@ -1,4 +1,4 @@ -package seedu.address.logic.commands; +package fasttrack.logic.commands; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -7,14 +7,19 @@ import org.junit.jupiter.api.Test; +import fasttrack.ui.ScreenType; + public class CommandResultTest { @Test public void equals() { - CommandResult commandResult = new CommandResult("feedback"); + CommandResult commandResult = new CommandResult( + "feedback", ScreenType.EXPENSE_SCREEN); // same values -> returns true - assertTrue(commandResult.equals(new CommandResult("feedback"))); - assertTrue(commandResult.equals(new CommandResult("feedback", false, false))); + assertTrue(commandResult.equals(new CommandResult( + "feedback", ScreenType.EXPENSE_SCREEN))); + assertTrue(commandResult.equals(new CommandResult( + "feedback", false, false, ScreenType.EXPENSE_SCREEN))); // same object -> returns true assertTrue(commandResult.equals(commandResult)); @@ -26,29 +31,34 @@ public void equals() { assertFalse(commandResult.equals(0.5f)); // different feedbackToUser value -> returns false - assertFalse(commandResult.equals(new CommandResult("different"))); + assertFalse(commandResult.equals(new CommandResult( + "different", ScreenType.EXPENSE_SCREEN))); // different showHelp value -> returns false - assertFalse(commandResult.equals(new CommandResult("feedback", true, false))); + assertFalse(commandResult.equals(new CommandResult( + "feedback", true, false, ScreenType.EXPENSE_SCREEN))); // different exit value -> returns false - assertFalse(commandResult.equals(new CommandResult("feedback", false, true))); + assertFalse(commandResult.equals(new CommandResult( + "feedback", false, true, ScreenType.EXPENSE_SCREEN))); } @Test public void hashcode() { - CommandResult commandResult = new CommandResult("feedback"); + CommandResult commandResult = new CommandResult("feedback", ScreenType.EXPENSE_SCREEN); // same values -> returns same hashcode - assertEquals(commandResult.hashCode(), new CommandResult("feedback").hashCode()); + assertEquals(commandResult.hashCode(), new CommandResult("feedback", ScreenType.EXPENSE_SCREEN).hashCode()); // different feedbackToUser value -> returns different hashcode - assertNotEquals(commandResult.hashCode(), new CommandResult("different").hashCode()); + assertNotEquals(commandResult.hashCode(), new CommandResult("different", ScreenType.EXPENSE_SCREEN).hashCode()); // different showHelp value -> returns different hashcode - assertNotEquals(commandResult.hashCode(), new CommandResult("feedback", true, false).hashCode()); + assertNotEquals(commandResult.hashCode(), new CommandResult( + "feedback", true, false, ScreenType.EXPENSE_SCREEN).hashCode()); // different exit value -> returns different hashcode - assertNotEquals(commandResult.hashCode(), new CommandResult("feedback", false, true).hashCode()); + assertNotEquals(commandResult.hashCode(), new CommandResult( + "feedback", false, true, ScreenType.EXPENSE_SCREEN).hashCode()); } } diff --git a/src/test/java/fasttrack/logic/commands/CommandTestUtil.java b/src/test/java/fasttrack/logic/commands/CommandTestUtil.java new file mode 100644 index 00000000000..094a32f31c1 --- /dev/null +++ b/src/test/java/fasttrack/logic/commands/CommandTestUtil.java @@ -0,0 +1,162 @@ +package fasttrack.logic.commands; + +import static fasttrack.logic.parser.CliSyntax.PREFIX_CATEGORY; +import static fasttrack.logic.parser.CliSyntax.PREFIX_DATE; +import static fasttrack.logic.parser.CliSyntax.PREFIX_END_DATE; +import static fasttrack.logic.parser.CliSyntax.PREFIX_NAME; +import static fasttrack.logic.parser.CliSyntax.PREFIX_PRICE; +import static fasttrack.logic.parser.CliSyntax.PREFIX_START_DATE; +import static fasttrack.logic.parser.CliSyntax.PREFIX_SUMMARY; +import static fasttrack.logic.parser.CliSyntax.PREFIX_TIMESPAN; +import static fasttrack.testutil.Assert.assertThrows; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.List; + +import fasttrack.commons.core.index.Index; +import fasttrack.logic.commands.exceptions.CommandException; +import fasttrack.model.ExpenseTracker; +import fasttrack.model.Model; +import fasttrack.model.expense.Expense; +import fasttrack.ui.ScreenType; + +/** + * Contains helper methods for testing commands. + */ +public class CommandTestUtil { + + public static final String VALID_NAME_APPLE = "Apple"; + public static final String VALID_NAME_BANANA = "Banana"; + public static final String VALID_NAME_CHERRY = "Cherry"; + public static final String VALID_NAME_DURIAN = "Durian"; + public static final String VALID_NAME_ELDERBERRY = "Elderberry"; + public static final String VALID_NAME_FIG = "Fig"; + public static final String VALID_NAME_GRAPE = "Grape"; + + public static final double VALID_PRICE_APPLE = 1.50; + public static final double VALID_PRICE_BANANA = 1.00; + public static final double VALID_PRICE_CHERRY = 0.20; + public static final double VALID_PRICE_DURIAN = 15.0; + public static final double VALID_PRICE_ELDERBERRY = 4.0; + public static final double VALID_PRICE_FIG = 1000.0; + public static final double VALID_PRICE_GRAPE = 10.0; + + public static final String VALID_CATEGORY_FOOD = "food"; + public static final String VALID_SUMMARY_FOOD = "For consumable expenses"; + public static final String VALID_CATEGORY_TECH = "tech"; + public static final String VALID_CATEGORY_SCHOOL = "school"; + + + public static final String VALID_DATE_APPLE = "1/3/2023"; + public static final String VALID_DATE_BANANA = "2/3/2023"; + public static final String VALID_DATE_CHERRY = "1/3/2023"; + public static final String VALID_DATE_DURIAN = "15/3/2023"; + public static final String VALID_DATE_ELDERBERRY = "1/1/2023"; + public static final String VALID_DATE_FIG = "15/2/2023"; + public static final String VALID_DATE_GRAPE = "17/3/2023"; + + + public static final String VALID_INTERVAL_DAY = " " + PREFIX_TIMESPAN + "day"; + public static final String VALID_INTERVAL_WEEK = " " + PREFIX_TIMESPAN + "week"; + public static final String VALID_INTERVAL_MONTH = " " + PREFIX_TIMESPAN + "month"; + public static final String VALID_INTERVAL_YEAR = " " + PREFIX_TIMESPAN + "year"; + public static final String INVALID_INTERVAL = " " + PREFIX_TIMESPAN + "biweekly"; + + + public static final String VALID_START_DATE = " " + PREFIX_START_DATE + "1/3/2023"; + public static final String VALID_END_DATE = " " + PREFIX_END_DATE + "2/3/2024"; + public static final String INVALID_DATE = " " + PREFIX_START_DATE + "50/13/2023"; + public static final String INVALID_START_DATE = " " + PREFIX_END_DATE + "1/3/2023"; + public static final String INVALID_END_DATE = " " + PREFIX_START_DATE + "2/3/2024"; + + + + public static final String DESC_APPLE = " " + PREFIX_NAME + VALID_NAME_APPLE; + public static final String AMT_APPLE = " " + PREFIX_PRICE + VALID_PRICE_APPLE; + public static final String DATE_APPLE = " " + PREFIX_DATE + VALID_DATE_APPLE; + public static final String CAT_APPLE = " " + PREFIX_CATEGORY + VALID_CATEGORY_FOOD; + public static final String SUM_APPLE = " " + PREFIX_SUMMARY + VALID_SUMMARY_FOOD; + + + public static final String DESC_BANANA = " " + PREFIX_NAME + VALID_NAME_BANANA; + public static final String AMT_BANANA = " " + PREFIX_PRICE + VALID_PRICE_BANANA; + public static final String DATE_BANANA = " " + PREFIX_DATE + VALID_DATE_BANANA; + public static final String CAT_BANANA = " " + PREFIX_CATEGORY + VALID_CATEGORY_FOOD; + + + public static final String INVALID_AMOUNT_DESC = " " + PREFIX_PRICE + "10x"; + // non-numeric character not allowed in amount + public static final String INVALID_DATE_FORMAT_DESC = " " + PREFIX_DATE + "2023/04/10"; + // invalid date format + public static final String INVALID_CATEGORY_DESC = " " + PREFIX_CATEGORY + "food@"; + // symbols in category not allowed + public static final String INVALID_CATEGORY_SUM = " " + PREFIX_SUMMARY + "For consumable expenses@"; + public static final String PREAMBLE_WHITESPACE = "\t \r \n"; + public static final String PREAMBLE_NON_EMPTY = "NonEmptyPreamble"; + + + /** + * Executes the given {@code command}, confirms that
+ * - the returned {@link CommandResult} matches {@code expectedCommandResult} + *
+ * - the {@code actualModel} matches {@code expectedModel} + */ + public static void assertCommandSuccess(Command command, Model actualDataModel, + CommandResult expectedCommandResult, + Model expectedDataModel) { + try { + CommandResult result = command.execute(actualDataModel); + assertEquals(expectedCommandResult, result); + assertEquals(expectedDataModel, actualDataModel); + } catch (CommandException ce) { + throw new AssertionError("Execution of command should not fail.", ce); + } + } + + /** + * Convenience wrapper to + * {@link #assertCommandSuccess(Command, Model, CommandResult, Model)} + * that takes a string {@code expectedMessage}. + */ + public static void assertCommandSuccess(Command command, Model actualDataModel, String expectedMessage, + Model expectedDataModel) { + CommandResult expectedCommandResult = new CommandResult(expectedMessage, ScreenType.EXPENSE_SCREEN); + assertCommandSuccess(command, actualDataModel, expectedCommandResult, expectedDataModel); + } + + /** + * Executes the given {@code command}, confirms that
+ * - a {@code CommandException} is thrown
+ * - the CommandException message matches {@code expectedMessage}
+ * - the address book, filtered person list and selected person in + * {@code actualModel} remain unchanged + */ + public static void assertCommandFailure(Command command, Model actualModel, String expectedMessage) { + // we are unable to defensively copy the model for comparison later, so we can + // only do so by copying its components. + ExpenseTracker expectedExpenseTracker = new ExpenseTracker(actualModel.getExpenseTracker()); + List expectedFilteredList = new ArrayList<>(actualModel.getFilteredExpenseList()); + + assertThrows(CommandException.class, expectedMessage, () -> command.execute(actualModel)); + assertEquals(expectedExpenseTracker, actualModel.getExpenseTracker()); + assertEquals(expectedFilteredList, actualModel.getFilteredExpenseList()); + } + + /** + * Updates {@code model}'s filtered list to show only the expense at the given + * {@code targetIndex} in the + * {@code model}'s ExpenseTracker. + */ + public static void showExpenseAtIndex(Model dataModel, Index targetIndex) { + assertTrue(targetIndex.getZeroBased() < dataModel.getFilteredExpenseList().size()); + + Expense expense = dataModel.getFilteredExpenseList().get(targetIndex.getZeroBased()); + final String name = expense.getName(); + //TODO update predicates here when created + //model.updateFilteredExpensesList(new NameContainsKeywordsPredicate(Arrays.asList(name))); + assertEquals(1, dataModel.getFilteredExpenseList().size()); + } + +} diff --git a/src/test/java/fasttrack/logic/commands/SetBudgetCommandTest.java b/src/test/java/fasttrack/logic/commands/SetBudgetCommandTest.java new file mode 100644 index 00000000000..d835ed953dc --- /dev/null +++ b/src/test/java/fasttrack/logic/commands/SetBudgetCommandTest.java @@ -0,0 +1,72 @@ +package fasttrack.logic.commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +import fasttrack.model.Budget; +import fasttrack.model.Model; +import fasttrack.model.ModelManager; +import fasttrack.model.UserPrefs; +import fasttrack.testutil.TypicalCategories; + +public class SetBudgetCommandTest { + private Budget budget = new Budget(1000); + private Budget differentBudget = new Budget(2000); + private Model model = new ModelManager(TypicalCategories.getTypicalExpenseTracker(), + new UserPrefs()); + + @Test + public void equals() { + SetBudgetCommand setBudgetCommand = new SetBudgetCommand(budget); + SetBudgetCommand setBudgetCommandCopy = new SetBudgetCommand(budget); + SetBudgetCommand setBudgetCommandDifferentBudget = new SetBudgetCommand(differentBudget); + + // same object -> returns true + assertTrue(setBudgetCommand.equals(setBudgetCommand)); + + // same values -> returns true + assertTrue(setBudgetCommand.equals(setBudgetCommandCopy)); + + // null -> returns false + assertFalse(setBudgetCommand.equals(null)); + + // different budget -> returns false + assertFalse(setBudgetCommand.equals(setBudgetCommandDifferentBudget)); + } + + @Test + public void testHashCode() { + SetBudgetCommand setBudgetCommand = new SetBudgetCommand(budget); + SetBudgetCommand setBudgetCommandCopy = new SetBudgetCommand(budget); + SetBudgetCommand setBudgetCommandDifferentBudget = new SetBudgetCommand(differentBudget); + + // same object -> returns same hashcode + assertEquals(setBudgetCommand.hashCode(), setBudgetCommand.hashCode()); + + // same values -> returns same hashcode + assertEquals(setBudgetCommand.hashCode(), setBudgetCommandCopy.hashCode()); + + // different budget -> returns different hashcode + assertNotEquals(setBudgetCommand.hashCode(), setBudgetCommandDifferentBudget.hashCode()); + } + + @Test + public void testToString() { + SetBudgetCommand setBudgetCommand = new SetBudgetCommand(budget); + assertEquals("SetBudgetCommand{budget=1000.0}", setBudgetCommand.toString()); + } + + @Test + public void execute_validBudget_success() { + SetBudgetCommand setBudgetCommand = new SetBudgetCommand(budget); + String expectedMessage = SetBudgetCommand.MESSAGE_SUCCESS + budget.toString(); + Model expectedModel = new ModelManager(model.getExpenseTracker(), new UserPrefs()); + CommandResult message = setBudgetCommand.execute(expectedModel); + assertEquals(expectedMessage, message.getFeedbackToUser()); + } + +} diff --git a/src/test/java/fasttrack/logic/commands/add/AddCategoryCommandTest.java b/src/test/java/fasttrack/logic/commands/add/AddCategoryCommandTest.java new file mode 100644 index 00000000000..61aab618dfc --- /dev/null +++ b/src/test/java/fasttrack/logic/commands/add/AddCategoryCommandTest.java @@ -0,0 +1,41 @@ +package fasttrack.logic.commands.add; + +import static java.util.Objects.requireNonNull; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +import fasttrack.logic.commands.exceptions.CommandException; +import fasttrack.model.Model; +import fasttrack.model.ModelManager; +import fasttrack.model.category.UserDefinedCategory; + +public class AddCategoryCommandTest { + private UserDefinedCategory toAdd = new UserDefinedCategory("test", "test"); + private UserDefinedCategory toAdd2 = new UserDefinedCategory("test2", "test2"); + private Model model = new ModelManager(); + private AddCategoryCommand addCategoryCommand = new AddCategoryCommand(toAdd); + private AddCategoryCommand addCategoryCommand2 = new AddCategoryCommand(toAdd2); + + @Test + public void addCategoryCommandTest() throws CommandException { + requireNonNull(model); + addCategoryCommand.execute(model); + addCategoryCommand2.execute(model); + assertEquals(model.getFilteredCategoryList().get(0), toAdd); + assertEquals(model.getFilteredCategoryList().get(1), toAdd2); + } + + @Test + public void addSameCategoryCommandTest() throws CommandException { + requireNonNull(model); + addCategoryCommand.execute(model); + assertThrows(CommandException.class, () -> addCategoryCommand.execute(model)); + } + + @Test + public void addCategoryCommandNullTest() { + assertThrows(NullPointerException.class, () -> new AddCategoryCommand(null)); + } +} diff --git a/src/test/java/fasttrack/logic/commands/add/AddExpenseCommandTest.java b/src/test/java/fasttrack/logic/commands/add/AddExpenseCommandTest.java new file mode 100644 index 00000000000..8ee05bc70f0 --- /dev/null +++ b/src/test/java/fasttrack/logic/commands/add/AddExpenseCommandTest.java @@ -0,0 +1,44 @@ +package fasttrack.logic.commands.add; + +import static java.util.Objects.requireNonNull; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.time.LocalDate; + +import org.junit.jupiter.api.Test; + +import fasttrack.logic.commands.exceptions.CommandException; +import fasttrack.model.Model; +import fasttrack.model.ModelManager; +import fasttrack.model.category.Category; +import fasttrack.model.category.UserDefinedCategory; +import fasttrack.model.expense.Expense; + +public class AddExpenseCommandTest { + private Model model = new ModelManager(); + private Category category = new UserDefinedCategory("test", "test"); + private LocalDate date = LocalDate.now(); + private Expense expense = new Expense("test", "1.0", date, category); + private Expense expense2 = new Expense("test2", "2.0", date, category); + private Expense expense3 = new Expense("test3", "3.0", date, category); + private AddExpenseCommand addExpenseCommand = new AddExpenseCommand(expense); + private AddExpenseCommand addExpenseCommand2 = new AddExpenseCommand(expense2); + private AddExpenseCommand addExpenseCommand3 = new AddExpenseCommand(expense3); + + @Test + public void addExpenseCommandTest() throws CommandException { + requireNonNull(model); + addExpenseCommand.execute(model); + addExpenseCommand2.execute(model); + addExpenseCommand3.execute(model); + assertEquals(model.getFilteredExpenseList().get(0), expense); + assertEquals(model.getFilteredExpenseList().get(1), expense2); + assertEquals(model.getFilteredExpenseList().get(2), expense3); + } + + @Test + public void addExpenseCommandNullTest() { + assertThrows(NullPointerException.class, () -> new AddExpenseCommand(null)); + } +} diff --git a/src/test/java/fasttrack/logic/commands/add/AddRecurringExpenseCommandTest.java b/src/test/java/fasttrack/logic/commands/add/AddRecurringExpenseCommandTest.java new file mode 100644 index 00000000000..91b852c2371 --- /dev/null +++ b/src/test/java/fasttrack/logic/commands/add/AddRecurringExpenseCommandTest.java @@ -0,0 +1,41 @@ +package fasttrack.logic.commands.add; + +import static java.util.Objects.requireNonNull; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.time.LocalDate; + +import org.junit.jupiter.api.Test; + +import fasttrack.logic.commands.exceptions.CommandException; +import fasttrack.model.Model; +import fasttrack.model.ModelManager; +import fasttrack.model.category.Category; +import fasttrack.model.category.UserDefinedCategory; +import fasttrack.model.expense.Expense; +import fasttrack.model.expense.RecurringExpenseManager; +import fasttrack.model.expense.RecurringExpenseType; + +public class AddRecurringExpenseCommandTest { + private Model model = new ModelManager(); + private Category category = new UserDefinedCategory("test", "test"); + private LocalDate currentDate = LocalDate.now(); + private LocalDate endDate = currentDate.plusMonths(1); + private LocalDate startDate = currentDate.minusMonths(1); + private Expense expense = new Expense("test", "4.5", currentDate, category); + private RecurringExpenseManager recurringExpense = new RecurringExpenseManager("test", 4.5, + category, startDate, endDate, RecurringExpenseType.MONTHLY); + + private AddRecurringExpenseCommand addRecurringExpenseCommand = new AddRecurringExpenseCommand(recurringExpense); + + @Test + public void addRecurringExpenseCommandTest() throws CommandException { + requireNonNull(model); + addRecurringExpenseCommand.execute(model); + assertEquals(model.getFilteredExpenseList().get(0), expense); + assertEquals(model.getFilteredExpenseList().size(), 2); + assertEquals(model.getFilteredExpenseList().get(0).getAmount(), 4.5); + assertEquals(model.getFilteredExpenseList().get(1).getDate(), startDate); + assertEquals(model.getFilteredExpenseList().get(0).getDate(), currentDate); + } +} diff --git a/src/test/java/fasttrack/logic/commands/delete/DeleteCategoryCommandTest.java b/src/test/java/fasttrack/logic/commands/delete/DeleteCategoryCommandTest.java new file mode 100644 index 00000000000..feaf6c9ad84 --- /dev/null +++ b/src/test/java/fasttrack/logic/commands/delete/DeleteCategoryCommandTest.java @@ -0,0 +1,40 @@ +package fasttrack.logic.commands.delete; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +import fasttrack.commons.core.index.Index; +import fasttrack.logic.commands.exceptions.CommandException; +import fasttrack.model.Model; +import fasttrack.model.ModelManager; +import fasttrack.model.category.Category; +import fasttrack.model.category.UserDefinedCategory; + +public class DeleteCategoryCommandTest { + private Model model = new ModelManager(); + private Category toDelete = new UserDefinedCategory("test", "test"); + private Category toDelete2 = new UserDefinedCategory("test2", "test2"); + + @Test + public void execute_validIndexUnfilteredList_success() throws CommandException { + model.addCategory(toDelete); + model.addCategory(toDelete2); + DeleteCategoryCommand deleteCategoryCommand = new DeleteCategoryCommand(Index.fromOneBased(1)); + deleteCategoryCommand.execute(model); + Model expectedModel = new ModelManager(); + expectedModel.addCategory(toDelete2); + assertEquals(expectedModel.getFilteredCategoryList(), model.getFilteredCategoryList()); + } + + @Test + public void execute_invalidIndexUnfilteredList_failure() throws CommandException { + model.addCategory(toDelete); + model.addCategory(toDelete2); + DeleteCategoryCommand deleteCategoryCommand = new DeleteCategoryCommand(Index.fromOneBased(3)); + // command should throw an exception + assertThrows(CommandException.class, () -> deleteCategoryCommand.execute(model)); + } + +} diff --git a/src/test/java/fasttrack/logic/commands/delete/DeleteExpenseCommandTest.java b/src/test/java/fasttrack/logic/commands/delete/DeleteExpenseCommandTest.java new file mode 100644 index 00000000000..9302704fb80 --- /dev/null +++ b/src/test/java/fasttrack/logic/commands/delete/DeleteExpenseCommandTest.java @@ -0,0 +1,48 @@ +package fasttrack.logic.commands.delete; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.time.LocalDate; + +import org.junit.jupiter.api.Test; + +import fasttrack.commons.core.index.Index; +import fasttrack.logic.commands.exceptions.CommandException; +import fasttrack.model.Model; +import fasttrack.model.ModelManager; +import fasttrack.model.category.MiscellaneousCategory; +import fasttrack.model.expense.Expense; + +public class DeleteExpenseCommandTest { + + private Expense expenseToDelete = new Expense("test", "1.0", + LocalDate.now(), new MiscellaneousCategory()); + private Expense expenseToDelete2 = new Expense("test2", "2.0", + LocalDate.now(), new MiscellaneousCategory()); + private Index firstExpenseIdx = Index.fromOneBased(1); + + @Test + public void execute_validIndexUnfilteredList_success() throws CommandException { + Model model = new ModelManager(); + model.addExpense(expenseToDelete); + model.addExpense(expenseToDelete2); + DeleteExpenseCommand deleteExpenseCommand = new DeleteExpenseCommand(firstExpenseIdx); + deleteExpenseCommand.execute(model); + Model expectedModel = new ModelManager(); + expectedModel.addExpense(expenseToDelete2); + assertEquals(expectedModel.getFilteredExpenseList(), model.getFilteredExpenseList()); + } + + @Test + public void execute_invalidIndexUnfilteredList_failure() throws CommandException { + Model model = new ModelManager(); + model.addExpense(expenseToDelete); + model.addExpense(expenseToDelete2); + DeleteExpenseCommand deleteExpenseCommand = new DeleteExpenseCommand( + Index.fromOneBased(3)); + // command should throw an exception + assertThrows(CommandException.class, () -> deleteExpenseCommand.execute(model)); + } + +} diff --git a/src/test/java/fasttrack/logic/commands/delete/DeleteRecurringExpenseCommandTest.java b/src/test/java/fasttrack/logic/commands/delete/DeleteRecurringExpenseCommandTest.java new file mode 100644 index 00000000000..38b2ec45501 --- /dev/null +++ b/src/test/java/fasttrack/logic/commands/delete/DeleteRecurringExpenseCommandTest.java @@ -0,0 +1,50 @@ +package fasttrack.logic.commands.delete; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.time.LocalDate; + +import org.junit.jupiter.api.Test; + +import fasttrack.commons.core.index.Index; +import fasttrack.logic.commands.exceptions.CommandException; +import fasttrack.model.Model; +import fasttrack.model.ModelManager; +import fasttrack.model.category.Category; +import fasttrack.model.category.UserDefinedCategory; +import fasttrack.model.expense.RecurringExpenseManager; +import fasttrack.model.expense.RecurringExpenseType; + +public class DeleteRecurringExpenseCommandTest { + + private Category category = new UserDefinedCategory("test", "test"); + private LocalDate currentDate = LocalDate.now(); + private LocalDate endDate = currentDate.plusMonths(1); + private LocalDate startDate = currentDate.minusMonths(1); + + private RecurringExpenseManager recurringExpense = new RecurringExpenseManager("test", 4.5, + category, startDate, endDate, RecurringExpenseType.MONTHLY); + private Index firstExpenseIdx = Index.fromOneBased(1); + + @Test + public void execute_validIndexUnfilteredList_success() throws CommandException { + Model model = new ModelManager(); + model.addRecurringGenerator(recurringExpense); + DeleteRecurringExpenseCommand deleteRecurringExpenseCommand = new + DeleteRecurringExpenseCommand(firstExpenseIdx); + deleteRecurringExpenseCommand.execute(model); + Model expectedModel = new ModelManager(); + assertEquals(expectedModel.getRecurringExpenseGenerators(), model.getRecurringExpenseGenerators()); + } + + @Test + public void execute_invalidIndexUnfilteredList_failure() throws CommandException { + Model model = new ModelManager(); + model.addRecurringGenerator(recurringExpense); + DeleteRecurringExpenseCommand deleteRecurringExpenseCommand = new DeleteRecurringExpenseCommand( + Index.fromOneBased(3)); + // command should throw an exception + assertThrows(CommandException.class, () -> deleteRecurringExpenseCommand.execute(model)); + } +} diff --git a/src/test/java/fasttrack/logic/commands/edit/EditCategoryCommandTest.java b/src/test/java/fasttrack/logic/commands/edit/EditCategoryCommandTest.java new file mode 100644 index 00000000000..0664a167fed --- /dev/null +++ b/src/test/java/fasttrack/logic/commands/edit/EditCategoryCommandTest.java @@ -0,0 +1,37 @@ +package fasttrack.logic.commands.edit; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +import fasttrack.commons.core.index.Index; +import fasttrack.logic.commands.exceptions.CommandException; +import fasttrack.model.Model; +import fasttrack.model.ModelManager; +import fasttrack.model.category.Category; +import fasttrack.model.category.UserDefinedCategory; + +public class EditCategoryCommandTest { + + private Model model = new ModelManager(); + private Category firstCat = new UserDefinedCategory("test", "test"); + private Category catToBe = new UserDefinedCategory("newCat", "test2"); + @Test + public void execute_validInput_success() throws CommandException { + model.addCategory(firstCat); + EditCategoryCommand editCategoryCommand = new EditCategoryCommand(Index.fromOneBased(1), + catToBe.getCategoryName(), catToBe.getSummary()); + editCategoryCommand.execute(model); + Model expectedModel = new ModelManager(); + expectedModel.addCategory(catToBe); + assertEquals(expectedModel.getFilteredCategoryList(), model.getFilteredCategoryList()); + } + + @Test + public void execute_invalidInput_failure() throws CommandException { + model.addCategory(firstCat); + EditCategoryCommand editCategoryCommand = new EditCategoryCommand(Index.fromOneBased(2), "test", "test"); + assertThrows(CommandException.class, () -> editCategoryCommand.execute(model)); + } +} diff --git a/src/test/java/fasttrack/logic/commands/edit/EditExpenseCommandTest.java b/src/test/java/fasttrack/logic/commands/edit/EditExpenseCommandTest.java new file mode 100644 index 00000000000..65c03b0f4cc --- /dev/null +++ b/src/test/java/fasttrack/logic/commands/edit/EditExpenseCommandTest.java @@ -0,0 +1,112 @@ +package fasttrack.logic.commands.edit; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.time.LocalDate; + +import org.junit.jupiter.api.Test; + +import fasttrack.commons.core.index.Index; +import fasttrack.logic.commands.exceptions.CommandException; +import fasttrack.model.Model; +import fasttrack.model.ModelManager; +import fasttrack.model.category.Category; +import fasttrack.model.category.UserDefinedCategory; +import fasttrack.model.expense.Expense; + +public class EditExpenseCommandTest { + + private Model model; + private Category category = new UserDefinedCategory("test", "test"); + private Category newCategory = new UserDefinedCategory("newCat", "test2"); + private LocalDate date = LocalDate.now(); + private Expense expense = new Expense("expense", "4.00", date, + category); + + + @Test + public void execute_validInput_success() throws CommandException { + model = new ModelManager(); + model.addCategory(category); + model.addExpense(expense); + EditExpenseCommand editExpenseCommand = new EditExpenseCommand(Index.fromOneBased(1), + "NewExpenseName", null, null, + category.getCategoryName()); + editExpenseCommand.execute(model); + assertEquals("NewExpenseName", model.getFilteredExpenseList().get(0).getName()); + } + + @Test + public void execute_invalidIndexInput() throws CommandException { + model = new ModelManager(); + model.addCategory(category); + model.addExpense(expense); + EditExpenseCommand editExpenseCommand = new EditExpenseCommand(Index.fromOneBased(2), + "NewExpenseName", null, null, + category.getCategoryName()); + assertThrows(CommandException.class, () -> editExpenseCommand.execute(model)); + } + + @Test + public void execute_invalidCategoryInput() throws CommandException { + model = new ModelManager(); + model.addCategory(category); + model.addExpense(expense); + EditExpenseCommand editExpenseCommand = new EditExpenseCommand(Index.fromOneBased(1), + null, null, null, + "Nonexistent"); + assertThrows(CommandException.class, () -> editExpenseCommand.execute(model)); + } + + @Test + public void execute_invalidInputAll() throws CommandException { + model = new ModelManager(); + model.addCategory(category); + model.addExpense(expense); + EditExpenseCommand editExpenseCommand = new EditExpenseCommand(Index.fromOneBased(1), + null, null, null, + null); + assertThrows(CommandException.class, () -> editExpenseCommand.execute(model)); + } + + @Test + public void execute_testSameObjectEquals() { + model = new ModelManager(); + model.addCategory(category); + model.addExpense(expense); + EditExpenseCommand editExpenseCommand = new EditExpenseCommand(Index.fromOneBased(1), + "NewExpenseName", null, null, + category.getCategoryName()); + assertEquals(editExpenseCommand, editExpenseCommand); + } + + @Test + public void execute_testEquals() { + model = new ModelManager(); + model.addCategory(category); + model.addExpense(expense); + EditExpenseCommand editExpenseCommand = new EditExpenseCommand(Index.fromOneBased(1), + "NewExpenseName", null, null, + category.getCategoryName()); + EditExpenseCommand sameExpenseCommand = new EditExpenseCommand(Index.fromOneBased(1), + "NewExpenseName", null, null, + category.getCategoryName()); + assertEquals(editExpenseCommand, sameExpenseCommand); + } + + @Test + public void execute_testNotEquals() { + model = new ModelManager(); + model.addCategory(category); + model.addExpense(expense); + EditExpenseCommand editExpenseCommand = new EditExpenseCommand(Index.fromOneBased(1), + "NewExpenseName", null, null, + category.getCategoryName()); + EditExpenseCommand sameExpenseCommand = new EditExpenseCommand(Index.fromOneBased(1), + "NewExpenseName", 200.0, null, + category.getCategoryName()); + assertNotEquals(editExpenseCommand, sameExpenseCommand); + } +} diff --git a/src/test/java/fasttrack/logic/commands/edit/EditRecurringExpenseManagerCommandTest.java b/src/test/java/fasttrack/logic/commands/edit/EditRecurringExpenseManagerCommandTest.java new file mode 100644 index 00000000000..78b3e5320b6 --- /dev/null +++ b/src/test/java/fasttrack/logic/commands/edit/EditRecurringExpenseManagerCommandTest.java @@ -0,0 +1,129 @@ +package fasttrack.logic.commands.edit; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.time.LocalDate; + +import org.junit.jupiter.api.Test; + +import fasttrack.commons.core.index.Index; +import fasttrack.logic.commands.exceptions.CommandException; +import fasttrack.model.Model; +import fasttrack.model.ModelManager; +import fasttrack.model.category.Category; +import fasttrack.model.category.UserDefinedCategory; +import fasttrack.model.expense.RecurringExpenseManager; +import fasttrack.model.expense.RecurringExpenseType; + +public class EditRecurringExpenseManagerCommandTest { + private Model model; + private Category category = new UserDefinedCategory("test", "test"); + private LocalDate currentDate = LocalDate.now(); + private LocalDate endDate = currentDate.plusMonths(1); + private LocalDate startDate = currentDate.minusMonths(1); + + private RecurringExpenseManager recurringExpense = new RecurringExpenseManager("test", 4.50, + category, startDate, endDate, RecurringExpenseType.MONTHLY); + @Test + public void execute_validNameChange_success() throws CommandException { + model = new ModelManager(); + model.addCategory(category); + model.addRecurringGenerator(recurringExpense); + EditRecurringExpenseManagerCommand editRecurringGeneratorCommand = new EditRecurringExpenseManagerCommand( + Index.fromOneBased(1), "standardName", null, null, + null, null); + + editRecurringGeneratorCommand.execute(model); + assertEquals(recurringExpense.getExpenseName(), "standardName"); + } + + @Test + public void execute_validAmountChange_success() throws CommandException { + model = new ModelManager(); + model.addCategory(category); + model.addRecurringGenerator(recurringExpense); + EditRecurringExpenseManagerCommand editRecurringGeneratorCommand = new EditRecurringExpenseManagerCommand( + Index.fromOneBased(1), null, 25.5, null, + null, null); + + editRecurringGeneratorCommand.execute(model); + assertEquals(recurringExpense.getAmount(), 25.5); + } + + @Test + public void execute_validCategoryChange_success() throws CommandException { + model = new ModelManager(); + model.addCategory(category); + Category toBeUsed = new UserDefinedCategory("placeholder category", "placeholder"); + model.addRecurringGenerator(recurringExpense); + model.addCategory(toBeUsed); + EditRecurringExpenseManagerCommand editRecurringGeneratorCommand = new EditRecurringExpenseManagerCommand( + Index.fromOneBased(1), null, null, toBeUsed.getCategoryName(), + null, null); + + editRecurringGeneratorCommand.execute(model); + assertEquals(recurringExpense.getExpenseCategory().getCategoryName(), toBeUsed.getCategoryName()); + + } + + @Test + public void execute_validFrequencyChange_success() throws CommandException { + model = new ModelManager(); + model.addCategory(category); + model.addRecurringGenerator(recurringExpense); + + EditRecurringExpenseManagerCommand editRecurringGeneratorCommand = new EditRecurringExpenseManagerCommand( + Index.fromOneBased(1), null, null, null, + "WEEKLY", null); + + editRecurringGeneratorCommand.execute(model); + assertEquals(recurringExpense.getRecurringExpenseType(), RecurringExpenseType.WEEKLY); + } + + @Test + public void execute_validEndDateChange_success() throws CommandException { + model = new ModelManager(); + model.addCategory(category); + model.addRecurringGenerator(recurringExpense); + EditRecurringExpenseManagerCommand editRecurringGeneratorCommand = new EditRecurringExpenseManagerCommand( + Index.fromOneBased(1), null, null, null, + null, LocalDate.parse("2023-12-03")); + + editRecurringGeneratorCommand.execute(model); + assertEquals(recurringExpense.getExpenseEndDate(), LocalDate.parse("2023-12-03")); + } + + @Test + public void execute_invalidIndexInput() throws CommandException { + model = new ModelManager(); + model.addCategory(category); + model.addRecurringGenerator(recurringExpense); + EditRecurringExpenseManagerCommand editRecurringGeneratorCommand = new EditRecurringExpenseManagerCommand( + Index.fromOneBased(3), null, null, null, + null, null); + assertThrows(CommandException.class, () -> editRecurringGeneratorCommand.execute(model)); + } + + @Test + public void execute_invalidCategoryInput() throws CommandException { + model = new ModelManager(); + model.addCategory(category); + model.addRecurringGenerator(recurringExpense); + EditRecurringExpenseManagerCommand editRecurringGeneratorCommand = new EditRecurringExpenseManagerCommand( + Index.fromOneBased(1), null, null, "Nonexistent", + null, null); + assertThrows(CommandException.class, () -> editRecurringGeneratorCommand.execute(model)); + } + + @Test + public void execute_invalidAllInput() throws CommandException { + model = new ModelManager(); + model.addCategory(category); + model.addRecurringGenerator(recurringExpense); + EditRecurringExpenseManagerCommand editRecurringGeneratorCommand = new EditRecurringExpenseManagerCommand( + Index.fromOneBased(1), null, null, null, + null, null); + assertThrows(CommandException.class, () -> editRecurringGeneratorCommand.execute(model)); + } +} diff --git a/src/test/java/fasttrack/logic/commands/general/CategorySummaryCommandTest.java b/src/test/java/fasttrack/logic/commands/general/CategorySummaryCommandTest.java new file mode 100644 index 00000000000..3dfd816826d --- /dev/null +++ b/src/test/java/fasttrack/logic/commands/general/CategorySummaryCommandTest.java @@ -0,0 +1,50 @@ +package fasttrack.logic.commands.general; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import fasttrack.commons.core.index.Index; +import fasttrack.logic.commands.CommandResult; +import fasttrack.logic.commands.exceptions.CommandException; +import fasttrack.model.Model; +import fasttrack.model.ModelManager; +import fasttrack.model.category.Category; +import fasttrack.model.category.MiscellaneousCategory; +import fasttrack.model.category.UserDefinedCategory; +import fasttrack.ui.ScreenType; + +public class CategorySummaryCommandTest { + + private Model dataModel = new ModelManager(); + private Category category = new UserDefinedCategory("Food", "Food"); + private Category miscCategory = new MiscellaneousCategory(); + + @BeforeEach + public void setUp() { + dataModel.addCategory(category); + dataModel.addCategory(miscCategory); + } + + @Test + public void execute_categorySummary_success() throws CommandException { + CommandResult expectedCommandResult = new CommandResult( + String.format(category.getCategoryName() + + " summary:\n" + category.getSummary()), + ScreenType.CATEGORY_SCREEN); + CategorySummaryCommand categorySummaryCommand = new CategorySummaryCommand(Index.fromOneBased(1)); + assertEquals(expectedCommandResult, categorySummaryCommand.execute(dataModel)); + } + + @Test + public void execute_categorySummaryMisc_success() throws CommandException { + CommandResult expectedCommandResult = new CommandResult( + String.format(miscCategory.getCategoryName() + + " summary:\n" + miscCategory.getSummary()), + ScreenType.CATEGORY_SCREEN); + + CategorySummaryCommand categorySummaryCommand = new CategorySummaryCommand(Index.fromOneBased(2)); + assertEquals(categorySummaryCommand.execute(dataModel), expectedCommandResult); + } +} diff --git a/src/test/java/fasttrack/logic/commands/general/ClearCommandTest.java b/src/test/java/fasttrack/logic/commands/general/ClearCommandTest.java new file mode 100644 index 00000000000..827b5760022 --- /dev/null +++ b/src/test/java/fasttrack/logic/commands/general/ClearCommandTest.java @@ -0,0 +1,24 @@ +package fasttrack.logic.commands.general; + +import static fasttrack.logic.commands.CommandTestUtil.assertCommandSuccess; + +import org.junit.jupiter.api.Test; + +import fasttrack.logic.commands.CommandResult; +import fasttrack.model.Model; +import fasttrack.model.ModelManager; +import fasttrack.ui.ScreenType; + +public class ClearCommandTest { + + private Model dataModel = new ModelManager(); + private Model expectedDataModel = new ModelManager(); + + @Test + public void execute_clear_success() { + CommandResult expectedCommandResult = new CommandResult(ClearCommand.MESSAGE_SUCCESS, false, false, + ScreenType.EXPENSE_SCREEN); + assertCommandSuccess(new ClearCommand(), dataModel, expectedCommandResult, expectedDataModel); + } + +} diff --git a/src/test/java/fasttrack/logic/commands/general/ExitCommandTest.java b/src/test/java/fasttrack/logic/commands/general/ExitCommandTest.java new file mode 100644 index 00000000000..2051db197e8 --- /dev/null +++ b/src/test/java/fasttrack/logic/commands/general/ExitCommandTest.java @@ -0,0 +1,23 @@ +package fasttrack.logic.commands.general; + +import static fasttrack.logic.commands.general.ExitCommand.MESSAGE_EXIT_ACKNOWLEDGEMENT; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +import fasttrack.logic.commands.CommandResult; +import fasttrack.model.Model; +import fasttrack.model.ModelManager; +import fasttrack.ui.ScreenType; + +public class ExitCommandTest { + private Model dataModel = new ModelManager(); + + @Test + public void execute_exit_success() { + CommandResult expectedCommandResult = new CommandResult( + MESSAGE_EXIT_ACKNOWLEDGEMENT, false, true, ScreenType.EXPENSE_SCREEN); + ExitCommand exitCommand = new ExitCommand(); + assertEquals(exitCommand.execute(dataModel), expectedCommandResult); + } +} diff --git a/src/test/java/fasttrack/logic/commands/general/FindCommandTest.java b/src/test/java/fasttrack/logic/commands/general/FindCommandTest.java new file mode 100644 index 00000000000..f4efaa659d6 --- /dev/null +++ b/src/test/java/fasttrack/logic/commands/general/FindCommandTest.java @@ -0,0 +1,81 @@ +package fasttrack.logic.commands.general; + +import static fasttrack.commons.core.Messages.MESSAGE_EXPENSES_LISTED_OVERVIEW; +import static fasttrack.logic.commands.CommandTestUtil.assertCommandSuccess; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; +import java.util.Collections; + +import org.junit.jupiter.api.Test; + +import fasttrack.model.Model; +import fasttrack.model.ModelManager; +import fasttrack.model.UserPrefs; +import fasttrack.model.expense.ExpenseContainsKeywordsPredicate; +import fasttrack.testutil.TypicalExpenses; + +/** + * Contains integration tests (interaction with the Model) for {@code FindCommand}. + */ +public class FindCommandTest { + private Model model = new ModelManager(TypicalExpenses.getTypicalExpenseTracker(), new UserPrefs()); + private Model expectedModel = new ModelManager(TypicalExpenses.getTypicalExpenseTracker(), new UserPrefs()); + + @Test + public void equals() { + ExpenseContainsKeywordsPredicate firstPredicate = + new ExpenseContainsKeywordsPredicate(Collections.singletonList("first")); + ExpenseContainsKeywordsPredicate secondPredicate = + new ExpenseContainsKeywordsPredicate(Collections.singletonList("second")); + + FindCommand findFirstCommand = new FindCommand(firstPredicate); + FindCommand findSecondCommand = new FindCommand(secondPredicate); + + // same object -> returns true + assertTrue(findFirstCommand.equals(findFirstCommand)); + + // same values -> returns true + FindCommand findFirstCommandCopy = new FindCommand(firstPredicate); + assertTrue(findFirstCommand.equals(findFirstCommandCopy)); + + // different types -> returns false + assertFalse(findFirstCommand.equals(1)); + + // null -> returns false + assertFalse(findFirstCommand.equals(null)); + + // different person -> returns false + assertFalse(findFirstCommand.equals(findSecondCommand)); + } + + @Test + public void execute_zeroKeywords_noExpenseFound() { + String expectedMessage = String.format(MESSAGE_EXPENSES_LISTED_OVERVIEW, 0); + ExpenseContainsKeywordsPredicate predicate = preparePredicate(" "); + FindCommand command = new FindCommand(predicate); + expectedModel.updateFilteredExpensesList(predicate); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(Collections.emptyList(), model.getFilteredExpenseList()); + } + + @Test + public void execute_multipleKeywords_multipleExpensesFound() { + String expectedMessage = String.format(MESSAGE_EXPENSES_LISTED_OVERVIEW, 3); + ExpenseContainsKeywordsPredicate predicate = preparePredicate("apple banana cherry"); + FindCommand command = new FindCommand(predicate); + expectedModel.updateFilteredExpensesList(predicate); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(Arrays.asList( + TypicalExpenses.BANANA, TypicalExpenses.APPLE, TypicalExpenses.CHERRY), model.getFilteredExpenseList()); + } + + /** + * Parses {@code userInput} into a {@code ExpenseContainsKeywordsPredicate}. + */ + private ExpenseContainsKeywordsPredicate preparePredicate(String userInput) { + return new ExpenseContainsKeywordsPredicate(Arrays.asList(userInput.split("\\s+"))); + } +} diff --git a/src/test/java/fasttrack/logic/commands/general/HelpCommandTest.java b/src/test/java/fasttrack/logic/commands/general/HelpCommandTest.java new file mode 100644 index 00000000000..aa4b14377c9 --- /dev/null +++ b/src/test/java/fasttrack/logic/commands/general/HelpCommandTest.java @@ -0,0 +1,23 @@ +package fasttrack.logic.commands.general; + +import static fasttrack.logic.commands.CommandTestUtil.assertCommandSuccess; +import static fasttrack.logic.commands.general.HelpCommand.SHOWING_HELP_MESSAGE; + +import org.junit.jupiter.api.Test; + +import fasttrack.logic.commands.CommandResult; +import fasttrack.model.Model; +import fasttrack.model.ModelManager; +import fasttrack.ui.ScreenType; + +public class HelpCommandTest { + private Model dataModel = new ModelManager(); + private Model expectedDataModel = new ModelManager(); + + @Test + public void execute_help_success() { + CommandResult expectedCommandResult = new CommandResult(SHOWING_HELP_MESSAGE, true, false, + ScreenType.EXPENSE_SCREEN); + assertCommandSuccess(new HelpCommand(), dataModel, expectedCommandResult, expectedDataModel); + } +} diff --git a/src/test/java/fasttrack/logic/commands/list/ListCategoriesCommandTest.java b/src/test/java/fasttrack/logic/commands/list/ListCategoriesCommandTest.java new file mode 100644 index 00000000000..0f43e7ed3fc --- /dev/null +++ b/src/test/java/fasttrack/logic/commands/list/ListCategoriesCommandTest.java @@ -0,0 +1,50 @@ +package fasttrack.logic.commands.list; + +import static fasttrack.logic.commands.CommandTestUtil.assertCommandSuccess; +import static fasttrack.logic.commands.list.ListCategoryCommand.MESSAGE_SUCCESS; +import static fasttrack.model.Model.PREDICATE_SHOW_ALL_EXPENSES; +import static fasttrack.testutil.TypicalExpenses.APPLE; +import static fasttrack.testutil.TypicalExpenses.BANANA; +import static fasttrack.testutil.TypicalExpenses.CHERRY; +import static fasttrack.testutil.TypicalExpenses.DURIAN; +import static fasttrack.testutil.TypicalExpenses.ELDERBERRY; +import static fasttrack.testutil.TypicalExpenses.FIG; +import static fasttrack.testutil.TypicalExpenses.GRAPE; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Arrays; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import fasttrack.model.Model; +import fasttrack.model.ModelManager; +import fasttrack.model.UserPrefs; +import fasttrack.testutil.TypicalExpenses; + +public class ListCategoriesCommandTest { + + private Model model; + private Model expectedModel; + + @BeforeEach + public void setUp() { + model = new ModelManager(TypicalExpenses.getTypicalExpenseTracker(), new UserPrefs()); + expectedModel = new ModelManager(model.getExpenseTracker(), new UserPrefs()); + } + + @Test + public void execute_expenseListUpdated_success() { + String expectedMessage = String.format(MESSAGE_SUCCESS); + ListCategoryCommand command = new ListCategoryCommand(); + + expectedModel.updateFilteredExpensesList(PREDICATE_SHOW_ALL_EXPENSES); + + assertCommandSuccess(command, model, expectedMessage, expectedModel); + + assertEquals( + Arrays.asList(GRAPE, DURIAN, BANANA, APPLE, CHERRY, FIG, ELDERBERRY), + model.getFilteredExpenseList()); + + } +} diff --git a/src/test/java/fasttrack/logic/commands/list/ListExpensesCommandTest.java b/src/test/java/fasttrack/logic/commands/list/ListExpensesCommandTest.java new file mode 100644 index 00000000000..9c7812124f8 --- /dev/null +++ b/src/test/java/fasttrack/logic/commands/list/ListExpensesCommandTest.java @@ -0,0 +1,88 @@ +package fasttrack.logic.commands.list; + +import static fasttrack.commons.core.Messages.MESSAGE_EXPENSES_LISTED_OVERVIEW; +import static fasttrack.logic.commands.CommandTestUtil.assertCommandSuccess; +import static fasttrack.model.Model.PREDICATE_SHOW_ALL_EXPENSES; +import static fasttrack.testutil.TypicalCategories.FOOD; +import static fasttrack.testutil.TypicalCategories.TECH; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.time.LocalDate; +import java.util.Arrays; +import java.util.Optional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import fasttrack.model.Model; +import fasttrack.model.ModelManager; +import fasttrack.model.UserPrefs; +import fasttrack.model.expense.ExpenseInCategoryPredicate; +import fasttrack.model.expense.ExpenseInTimespanPredicate; +import fasttrack.testutil.TypicalExpenses; + +/** + * Contains integration tests (interaction with the Model) and unit tests for + * ListCommand. + */ +public class ListExpensesCommandTest { + + private Model model; + private Model expectedModel; + + @BeforeEach + public void setUp() { + model = new ModelManager(TypicalExpenses.getTypicalExpenseTracker(), new UserPrefs()); + expectedModel = new ModelManager(model.getExpenseTracker(), new UserPrefs()); + } + + @Test + public void execute_listIsNotFiltered_showsSameList() { + + String expectedMessage = String.format(MESSAGE_EXPENSES_LISTED_OVERVIEW, 7); + ListExpensesCommand command = new ListExpensesCommand(Optional.empty(), Optional.empty()); + expectedModel.updateFilteredExpensesList(PREDICATE_SHOW_ALL_EXPENSES); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals( + Arrays.asList(TypicalExpenses.GRAPE, TypicalExpenses.DURIAN, TypicalExpenses.BANANA, + TypicalExpenses.APPLE, TypicalExpenses.CHERRY, TypicalExpenses.FIG, TypicalExpenses.ELDERBERRY), + model.getFilteredExpenseList()); + } + + @Test + public void execute_listFilterByCategory_showsCategory() { + String expectedMessage = String.format(MESSAGE_EXPENSES_LISTED_OVERVIEW, 2); + ExpenseInCategoryPredicate predicate = new ExpenseInCategoryPredicate(FOOD); + + ListExpensesCommand command = new ListExpensesCommand(Optional.of(predicate), Optional.empty()); + expectedModel.updateFilteredExpensesList(predicate); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(Arrays.asList(TypicalExpenses.BANANA, TypicalExpenses.APPLE), model.getFilteredExpenseList()); + } + + @Test + public void execute_listFilterByTimespan_showsExpenses() { + String expectedMessage = String.format(MESSAGE_EXPENSES_LISTED_OVERVIEW, 2); + ExpenseInTimespanPredicate predicate = new ExpenseInTimespanPredicate(LocalDate.of(2023, 3, 13)); + + ListExpensesCommand command = new ListExpensesCommand(Optional.empty(), Optional.of(predicate)); + expectedModel.updateFilteredExpensesList(predicate); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(Arrays.asList(TypicalExpenses.GRAPE, TypicalExpenses.DURIAN), + model.getFilteredExpenseList()); + } + + @Test + public void execute_listFilterByCategoryByTimespan_showsExpense() { + String expectedMessage = String.format(MESSAGE_EXPENSES_LISTED_OVERVIEW, 1); + ExpenseInTimespanPredicate timespanPredicate = new ExpenseInTimespanPredicate(LocalDate.of(2023, 3, 13)); + ExpenseInCategoryPredicate categoryPredicate = new ExpenseInCategoryPredicate(TECH); + ListExpensesCommand command = new ListExpensesCommand(Optional.of(categoryPredicate), + Optional.of(timespanPredicate)); + expectedModel.updateFilteredExpensesList(categoryPredicate.and(timespanPredicate)); + + + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(Arrays.asList(TypicalExpenses.DURIAN), model.getFilteredExpenseList()); + } +} diff --git a/src/test/java/fasttrack/logic/commands/list/ListRecurringExpensesCommandTest.java b/src/test/java/fasttrack/logic/commands/list/ListRecurringExpensesCommandTest.java new file mode 100644 index 00000000000..daed4a108e0 --- /dev/null +++ b/src/test/java/fasttrack/logic/commands/list/ListRecurringExpensesCommandTest.java @@ -0,0 +1,50 @@ +package fasttrack.logic.commands.list; + +import static fasttrack.logic.commands.CommandTestUtil.assertCommandSuccess; +import static fasttrack.logic.commands.list.ListRecurringExpensesCommand.MESSAGE_SUCCESS; +import static fasttrack.model.Model.PREDICATE_SHOW_ALL_EXPENSES; +import static fasttrack.testutil.TypicalExpenses.APPLE; +import static fasttrack.testutil.TypicalExpenses.BANANA; +import static fasttrack.testutil.TypicalExpenses.CHERRY; +import static fasttrack.testutil.TypicalExpenses.DURIAN; +import static fasttrack.testutil.TypicalExpenses.ELDERBERRY; +import static fasttrack.testutil.TypicalExpenses.FIG; +import static fasttrack.testutil.TypicalExpenses.GRAPE; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Arrays; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import fasttrack.model.Model; +import fasttrack.model.ModelManager; +import fasttrack.model.UserPrefs; +import fasttrack.testutil.TypicalExpenses; + +public class ListRecurringExpensesCommandTest { + + private Model model; + private Model expectedModel; + + @BeforeEach + public void setUp() { + model = new ModelManager(TypicalExpenses.getTypicalExpenseTracker(), new UserPrefs()); + expectedModel = new ModelManager(model.getExpenseTracker(), new UserPrefs()); + } + + @Test + public void execute_expenseListUpdated_success() { + String expectedMessage = String.format(MESSAGE_SUCCESS); + ListRecurringExpensesCommand command = new ListRecurringExpensesCommand(); + + expectedModel.updateFilteredExpensesList(PREDICATE_SHOW_ALL_EXPENSES); + + assertCommandSuccess(command, model, expectedMessage, expectedModel); + + assertEquals( + Arrays.asList(GRAPE, DURIAN, BANANA, APPLE, CHERRY, FIG, ELDERBERRY), + model.getFilteredExpenseList()); + + } +} diff --git a/src/test/java/seedu/address/logic/parser/ArgumentTokenizerTest.java b/src/test/java/fasttrack/logic/parser/ArgumentTokenizerTest.java similarity index 99% rename from src/test/java/seedu/address/logic/parser/ArgumentTokenizerTest.java rename to src/test/java/fasttrack/logic/parser/ArgumentTokenizerTest.java index c97308935f5..bc8bc092449 100644 --- a/src/test/java/seedu/address/logic/parser/ArgumentTokenizerTest.java +++ b/src/test/java/fasttrack/logic/parser/ArgumentTokenizerTest.java @@ -1,4 +1,4 @@ -package seedu.address.logic.parser; +package fasttrack.logic.parser; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; diff --git a/src/test/java/fasttrack/logic/parser/CategorySummaryParserTest.java b/src/test/java/fasttrack/logic/parser/CategorySummaryParserTest.java new file mode 100644 index 00000000000..22b8165bae7 --- /dev/null +++ b/src/test/java/fasttrack/logic/parser/CategorySummaryParserTest.java @@ -0,0 +1,35 @@ +package fasttrack.logic.parser; + +import static fasttrack.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static fasttrack.logic.parser.CommandParserTestUtil.assertParseFailure; +import static fasttrack.logic.parser.CommandParserTestUtil.assertParseSuccess; +import static fasttrack.testutil.TypicalIndexes.INDEX_FIRST_PERSON; + +import org.junit.jupiter.api.Test; + +import fasttrack.logic.commands.general.CategorySummaryCommand; + +class CategorySummaryParserTest { + + private final CategorySummaryParser parser = new CategorySummaryParser(); + + @Test + void parse_validArgs_returnsCategorySummaryCommand() { + String input1 = "1"; + CategorySummaryCommand expected1 = new CategorySummaryCommand(INDEX_FIRST_PERSON); + assertParseSuccess(parser, input1, expected1); + } + + @Test + void parse_invalidValue_throwsParseException() { + // Invalid index + String input1 = "0"; + assertParseFailure(parser, input1, String.format(MESSAGE_INVALID_COMMAND_FORMAT, + CategorySummaryCommand.MESSAGE_USAGE)); + + // Missing index + String input2 = ""; + assertParseFailure(parser, input2, String.format(MESSAGE_INVALID_COMMAND_FORMAT, + CategorySummaryCommand.MESSAGE_USAGE)); + } +} diff --git a/src/test/java/seedu/address/logic/parser/CommandParserTestUtil.java b/src/test/java/fasttrack/logic/parser/CommandParserTestUtil.java similarity index 89% rename from src/test/java/seedu/address/logic/parser/CommandParserTestUtil.java rename to src/test/java/fasttrack/logic/parser/CommandParserTestUtil.java index 9bf1ccf1cef..b4816e522f3 100644 --- a/src/test/java/seedu/address/logic/parser/CommandParserTestUtil.java +++ b/src/test/java/fasttrack/logic/parser/CommandParserTestUtil.java @@ -1,9 +1,9 @@ -package seedu.address.logic.parser; +package fasttrack.logic.parser; import static org.junit.jupiter.api.Assertions.assertEquals; -import seedu.address.logic.commands.Command; -import seedu.address.logic.parser.exceptions.ParseException; +import fasttrack.logic.commands.Command; +import fasttrack.logic.parser.exceptions.ParseException; /** * Contains helper methods for testing command parsers. diff --git a/src/test/java/fasttrack/logic/parser/ExpenseTrackerParserTest.java b/src/test/java/fasttrack/logic/parser/ExpenseTrackerParserTest.java new file mode 100644 index 00000000000..7594793c3e8 --- /dev/null +++ b/src/test/java/fasttrack/logic/parser/ExpenseTrackerParserTest.java @@ -0,0 +1,88 @@ +package fasttrack.logic.parser; + +import static fasttrack.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static fasttrack.commons.core.Messages.MESSAGE_UNKNOWN_COMMAND; +import static fasttrack.testutil.Assert.assertThrows; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; + +import fasttrack.logic.commands.general.ExitCommand; +import fasttrack.logic.commands.general.FindCommand; +import fasttrack.logic.commands.general.HelpCommand; +import fasttrack.logic.parser.exceptions.ParseException; +import fasttrack.model.expense.ExpenseContainsKeywordsPredicate; + +public class ExpenseTrackerParserTest { + + private final ExpenseTrackerParser parser = new ExpenseTrackerParser(); + + @Test + public void parseCommand_add_category() throws Exception { + // AddCategoryCommand command = (AddCategoryCommand) parser.parseCommand( + // AddCategoryCommand.COMMAND_WORD + CliSyntax.PREFIX_CATEGORY + "food" + + // CliSyntax.PREFIX_SUMMARY + "food summary"); + // Category expectedCategory = new UserDefinedCategory("food", "food summary"); + // assertEquals(new AddCategoryCommand(expectedCategory), command); + } + + @Test + public void parseCommand_clear() throws Exception { + // assertTrue(parser.parseCommand(ClearCommand.COMMAND_WORD) instanceof + // ClearCommand); + // assertTrue(parser.parseCommand(ClearCommand.COMMAND_WORD + " 3") instanceof + // ClearCommand); + } + + @Test + public void parseCommand_delete() throws Exception { + // DeleteCommand command = (DeleteCommand) parser.parseCommand( + // DeleteCommand.COMMAND_WORD + " " + INDEX_FIRST_PERSON.getOneBased()); + // assertEquals(new DeleteCommand(INDEX_FIRST_PERSON), command); + } + + @Test + public void parseCommand_exit() throws Exception { + assertTrue(parser.parseCommand(ExitCommand.COMMAND_WORD) instanceof ExitCommand); + assertTrue(parser.parseCommand(ExitCommand.COMMAND_WORD + " 3") instanceof ExitCommand); + } + + @Test + public void parseCommand_find() throws Exception { + List keywords = Arrays.asList("foo", "bar", "baz"); + + FindCommand command = (FindCommand) parser.parseCommand( + FindCommand.COMMAND_WORD + " " + keywords.stream().collect(Collectors.joining(" "))); + assertEquals(new FindCommand(new ExpenseContainsKeywordsPredicate(keywords)), command); + } + + @Test + public void parseCommand_help() throws Exception { + assertTrue(parser.parseCommand(HelpCommand.COMMAND_WORD) instanceof HelpCommand); + assertTrue(parser.parseCommand(HelpCommand.COMMAND_WORD + " 3") instanceof HelpCommand); + } + + @Test + public void parseCommand_list() throws Exception { + // assertTrue(parser.parseCommand(ListExpensesCommand.COMMAND_WORD) instanceof + // ListExpensesCommand); + // assertTrue(parser.parseCommand(ListExpensesCommand.COMMAND_WORD + " 3") + // instanceof ListExpensesCommand); + } + + @Test + public void parseCommand_unrecognisedInput_throwsParseException() { + assertThrows(ParseException.class, String.format(MESSAGE_INVALID_COMMAND_FORMAT, + HelpCommand.MESSAGE_USAGE), () -> parser.parseCommand("")); + } + + @Test + public void parseCommand_unknownCommand_throwsParseException() { + assertThrows(ParseException.class, MESSAGE_UNKNOWN_COMMAND, () -> parser.parseCommand("unknownCommand")); + } +} diff --git a/src/test/java/fasttrack/logic/parser/FindCommandParserTest.java b/src/test/java/fasttrack/logic/parser/FindCommandParserTest.java new file mode 100644 index 00000000000..31a3305e190 --- /dev/null +++ b/src/test/java/fasttrack/logic/parser/FindCommandParserTest.java @@ -0,0 +1,34 @@ +package fasttrack.logic.parser; + +import static fasttrack.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static fasttrack.logic.parser.CommandParserTestUtil.assertParseFailure; +import static fasttrack.logic.parser.CommandParserTestUtil.assertParseSuccess; + +import java.util.Arrays; + +import org.junit.jupiter.api.Test; + +import fasttrack.logic.commands.general.FindCommand; +import fasttrack.model.expense.ExpenseContainsKeywordsPredicate; + +public class FindCommandParserTest { + + private FindCommandParser parser = new FindCommandParser(); + + @Test + public void parse_emptyArg_throwsParseException() { + assertParseFailure(parser, " ", String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE)); + } + + @Test + public void parse_validArgs_returnsFindCommand() { + // no leading and trailing whitespaces + FindCommand expectedFindCommand = + new FindCommand(new ExpenseContainsKeywordsPredicate(Arrays.asList("Apple", "Orange"))); + assertParseSuccess(parser, "Apple Orange", expectedFindCommand); + + // multiple whitespaces between keywords + assertParseSuccess(parser, " \n Apple \n \t Orange \t", expectedFindCommand); + } + +} diff --git a/src/test/java/fasttrack/logic/parser/ListCommandParserTest.java b/src/test/java/fasttrack/logic/parser/ListCommandParserTest.java new file mode 100644 index 00000000000..335750eee13 --- /dev/null +++ b/src/test/java/fasttrack/logic/parser/ListCommandParserTest.java @@ -0,0 +1,130 @@ +package fasttrack.logic.parser; + +import static fasttrack.logic.commands.CommandTestUtil.PREAMBLE_WHITESPACE; +import static fasttrack.logic.parser.CliSyntax.PREFIX_CATEGORY; +import static fasttrack.logic.parser.CliSyntax.PREFIX_TIMESPAN; +import static fasttrack.logic.parser.CommandParserTestUtil.assertParseSuccess; +import static fasttrack.logic.parser.ParserUtil.Timespan.MONTH; +import static fasttrack.logic.parser.ParserUtil.Timespan.WEEK; +import static fasttrack.logic.parser.ParserUtil.Timespan.YEAR; +import static fasttrack.testutil.TypicalCategories.FOOD; +import static fasttrack.testutil.TypicalCategories.TECH; + +import java.util.Optional; + +import org.junit.jupiter.api.Test; + +import fasttrack.logic.commands.list.ListExpensesCommand; +import fasttrack.model.expense.ExpenseInCategoryPredicate; +import fasttrack.model.expense.ExpenseInTimespanPredicate; + +public class ListCommandParserTest { + private ListCommandParser parser = new ListCommandParser(); + + @Test + public void parse_categoryFieldPresent_success() { + ExpenseInCategoryPredicate predicateFood = new ExpenseInCategoryPredicate(FOOD); + ExpenseInCategoryPredicate predicateTech = new ExpenseInCategoryPredicate(TECH); + ListExpensesCommand expectedCommandFood = new ListExpensesCommand(Optional.of(predicateFood), + Optional.empty()); + ListExpensesCommand expectedCommandTech = new ListExpensesCommand(Optional.of(predicateTech), + Optional.empty()); + + + assertParseSuccess(parser, " " + PREFIX_CATEGORY + FOOD.getCategoryName(), expectedCommandFood); + assertParseSuccess(parser, " " + PREFIX_CATEGORY + TECH.getCategoryName(), expectedCommandTech); + } + + @Test + public void parse_timespanFieldPresent_success() { + ExpenseInTimespanPredicate predicateWeek = new ExpenseInTimespanPredicate(WEEK); + ExpenseInTimespanPredicate predicateMonth = new ExpenseInTimespanPredicate(MONTH); + ExpenseInTimespanPredicate predicateYear = new ExpenseInTimespanPredicate(YEAR); + + ListExpensesCommand expectedCommandWeek = + new ListExpensesCommand(Optional.empty(), Optional.of(predicateWeek)); + ListExpensesCommand expectedCommandMonth = + new ListExpensesCommand(Optional.empty(), Optional.of(predicateMonth)); + ListExpensesCommand expectedCommandYear = + new ListExpensesCommand(Optional.empty(), Optional.of(predicateYear)); + + // spelt in full, lowercase + assertParseSuccess(parser, " " + PREFIX_TIMESPAN + "week", expectedCommandWeek); + assertParseSuccess(parser, " " + PREFIX_TIMESPAN + "month", expectedCommandMonth); + assertParseSuccess(parser, " " + PREFIX_TIMESPAN + "year", expectedCommandYear); + + // spelt in full, UPPERCASE + assertParseSuccess(parser, " " + PREFIX_TIMESPAN + "WEEK", expectedCommandWeek); + assertParseSuccess(parser, " " + PREFIX_TIMESPAN + "MONTH", expectedCommandMonth); + assertParseSuccess(parser, " " + PREFIX_TIMESPAN + "YEAR", expectedCommandYear); + + // spelt in full, Capitalized + assertParseSuccess(parser, " " + PREFIX_TIMESPAN + "Week", expectedCommandWeek); + assertParseSuccess(parser, " " + PREFIX_TIMESPAN + "Month", expectedCommandMonth); + assertParseSuccess(parser, " " + PREFIX_TIMESPAN + "Year", expectedCommandYear); + + // abbreviation, lowercase + assertParseSuccess(parser, " " + PREFIX_TIMESPAN + "w", expectedCommandWeek); + assertParseSuccess(parser, " " + PREFIX_TIMESPAN + "m", expectedCommandMonth); + assertParseSuccess(parser, " " + PREFIX_TIMESPAN + "y", expectedCommandYear); + + // abbreviation, uppercase + assertParseSuccess(parser, " " + PREFIX_TIMESPAN + "W", expectedCommandWeek); + assertParseSuccess(parser, " " + PREFIX_TIMESPAN + "M", expectedCommandMonth); + assertParseSuccess(parser, " " + PREFIX_TIMESPAN + "Y", expectedCommandYear); + } + + @Test + public void parse_categoryTimespanPresent_success() { + ExpenseInTimespanPredicate predicateWeek = new ExpenseInTimespanPredicate(WEEK); + ExpenseInTimespanPredicate predicateMonth = new ExpenseInTimespanPredicate(MONTH); + ExpenseInTimespanPredicate predicateYear = new ExpenseInTimespanPredicate(YEAR); + + ExpenseInCategoryPredicate categoryPredicateFood = new ExpenseInCategoryPredicate(FOOD); + ExpenseInCategoryPredicate categoryPredicateTech = new ExpenseInCategoryPredicate(TECH); + + ListExpensesCommand expectedCommandFoodYear = new ListExpensesCommand(Optional.of(categoryPredicateFood), + Optional.of(predicateYear)); + ListExpensesCommand expectedCommandTechWeek = new ListExpensesCommand(Optional.of(categoryPredicateTech), + Optional.of(predicateWeek)); + ListExpensesCommand expectedCommandFoodMonth = new ListExpensesCommand(Optional.of(categoryPredicateFood), + Optional.of(predicateMonth)); + + + // List by Food, Year + assertParseSuccess(parser, " " + PREFIX_TIMESPAN + "year" + " " + + PREFIX_CATEGORY + FOOD.getCategoryName(), + expectedCommandFoodYear); + assertParseSuccess(parser, " " + PREFIX_CATEGORY + + FOOD.getCategoryName() + " " + PREFIX_TIMESPAN + "year", + expectedCommandFoodYear); + + // List by Tech, Week + assertParseSuccess(parser, " " + PREFIX_TIMESPAN + "week" + " " + + PREFIX_CATEGORY + TECH.getCategoryName(), + expectedCommandTechWeek); + assertParseSuccess(parser, " " + PREFIX_CATEGORY + + TECH.getCategoryName() + " " + PREFIX_TIMESPAN + "w", + expectedCommandTechWeek); + + // List by Food, Month + assertParseSuccess(parser, " " + PREFIX_TIMESPAN + "m" + " " + + PREFIX_CATEGORY + FOOD.getCategoryName(), + expectedCommandFoodMonth); + assertParseSuccess(parser, " " + PREFIX_CATEGORY + + FOOD.getCategoryName() + " " + PREFIX_TIMESPAN + "MONTH", + expectedCommandFoodMonth); + } + + @Test + public void parse_noFields_success() { + ListExpensesCommand expectedCommand = new ListExpensesCommand(Optional.empty(), Optional.empty()); + + // whitespace preamble + assertParseSuccess(parser, PREAMBLE_WHITESPACE, expectedCommand); + + // no preamble + assertParseSuccess(parser, "", expectedCommand); + } + +} diff --git a/src/test/java/fasttrack/logic/parser/ParserUtilTest.java b/src/test/java/fasttrack/logic/parser/ParserUtilTest.java new file mode 100644 index 00000000000..dcf1fea4534 --- /dev/null +++ b/src/test/java/fasttrack/logic/parser/ParserUtilTest.java @@ -0,0 +1,190 @@ +package fasttrack.logic.parser; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.time.DayOfWeek; +import java.time.LocalDate; + +import org.junit.jupiter.api.Test; + +import fasttrack.commons.core.index.Index; +import fasttrack.logic.parser.exceptions.ParseException; +import fasttrack.model.category.MiscellaneousCategory; +import fasttrack.model.category.UserDefinedCategory; +import fasttrack.model.expense.Price; +import fasttrack.model.expense.RecurringExpenseType; + + +public class ParserUtilTest { + + @Test + public void parseIndex_validInput_success() throws ParseException { + assertEquals(Index.fromOneBased(1), ParserUtil.parseIndex(" 1 ")); + assertEquals(Index.fromOneBased(2), ParserUtil.parseIndex("2")); + } + + @Test + public void parseIndex_invalidInput_throwsParseException() { + // empty string + assertThrows(ParseException.class, () -> ParserUtil.parseIndex("")); + // string based input + assertThrows(ParseException.class, () -> ParserUtil.parseIndex("one")); + assertThrows(ParseException.class, () -> ParserUtil.parseIndex("0")); + assertThrows(ParseException.class, () -> ParserUtil.parseIndex("-1")); + } + + @Test + public void parseExpenseName_nullInput_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> ParserUtil.parseExpenseName(null)); + } + + @Test + public void parseExpenseName_validInput_success() throws ParseException { + assertEquals("milk", ParserUtil.parseExpenseName(" milk ")); + } + + @Test + public void parseExpenseName_invalidInput_throwsParseException() { + assertThrows(ParseException.class, () -> ParserUtil.parseExpenseName("")); + } + + @Test + public void parsePrice_nullInput_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> ParserUtil.parsePrice(null)); + } + + @Test + public void parsePrice_validInput_success() throws ParseException { + assertEquals(new Price("1.23"), ParserUtil.parsePrice(" 1.23 ")); + } + + @Test + public void parsePrice_invalidInput_throwsParseException() { + assertThrows(ParseException.class, () -> ParserUtil.parsePrice("")); + assertThrows(ParseException.class, () -> ParserUtil.parsePrice("-1.23")); + assertThrows(ParseException.class, () -> ParserUtil.parsePrice("one")); + assertThrows(ParseException.class, () -> ParserUtil.parsePrice("0")); + } + + @Test + public void parseCategoryWithSummary_validInput_success() throws ParseException { + // leading and trailing whitespace + assertEquals(new UserDefinedCategory("category", "abc"), ParserUtil.parseCategory(" category ", "abc")); + // miscellaneous category + assertEquals(new MiscellaneousCategory(), ParserUtil.parseCategory("miscellaneous")); + UserDefinedCategory category = ParserUtil.parseCategory("food", "for dining"); + assertEquals("food", category.getCategoryName()); + assertEquals("for dining", category.getSummary()); + } + + @Test + public void parseCategory_invalidInput_throwsParseException() { + // empty category name + assertThrows(ParseException.class, () -> ParserUtil.parseCategory("")); + assertThrows(ParseException.class, () -> ParserUtil.parseCategory("@food")); + } + + + @Test + void parseDate_validInput_success() throws ParseException { + LocalDate date = ParserUtil.parseDate("22/3/2023"); + assertEquals(LocalDate.parse("2023-03-22"), date); + } + + @Test + void parseDate_invalidInput_throwsParseException() { + assertThrows(ParseException.class, () -> { + ParserUtil.parseDate("2023/03/14"); + }); + assertThrows(ParseException.class, () -> { + ParserUtil.parseDate("2023-04-10"); + }); + + assertThrows(ParseException.class, () -> { + ParserUtil.parseDate("14-3-2023"); + }); + assertThrows(ParseException.class, () -> { + ParserUtil.parseDate("2/23/2023"); + }); + } + + @Test + void parseTimespan_validInput_success() throws ParseException { + assertEquals(ParserUtil.Timespan.WEEK, ParserUtil.parseTimespan("week")); + assertEquals(ParserUtil.Timespan.MONTH, ParserUtil.parseTimespan("month")); + assertEquals(ParserUtil.Timespan.YEAR, ParserUtil.parseTimespan("year")); + assertEquals(ParserUtil.Timespan.WEEK, ParserUtil.parseTimespan("w")); + assertEquals(ParserUtil.Timespan.MONTH, ParserUtil.parseTimespan("m")); + assertEquals(ParserUtil.Timespan.YEAR, ParserUtil.parseTimespan("y")); + assertEquals(ParserUtil.Timespan.WEEK, ParserUtil.parseTimespan("WEEK")); + assertEquals(ParserUtil.Timespan.MONTH, ParserUtil.parseTimespan("MONTH")); + assertEquals(ParserUtil.Timespan.YEAR, ParserUtil.parseTimespan("YEAR")); + assertEquals(ParserUtil.Timespan.WEEK, ParserUtil.parseTimespan("W")); + assertEquals(ParserUtil.Timespan.MONTH, ParserUtil.parseTimespan("M")); + assertEquals(ParserUtil.Timespan.YEAR, ParserUtil.parseTimespan("Y")); + assertEquals(ParserUtil.Timespan.WEEK, ParserUtil.parseTimespan("Week")); + assertEquals(ParserUtil.Timespan.MONTH, ParserUtil.parseTimespan("Month")); + assertEquals(ParserUtil.Timespan.YEAR, ParserUtil.parseTimespan("Year")); + } + + @Test + void parseTimespan_invalidInput_throwsParseException() { + assertThrows(ParseException.class, () -> { + ParserUtil.parseTimeSpanRecurringExpense("wk"); + }); + assertThrows(ParseException.class, () -> { + ParserUtil.parseTimeSpanRecurringExpense("mon"); + }); + assertThrows(ParseException.class, () -> { + ParserUtil.parseTimeSpanRecurringExpense(""); + }); + assertThrows(ParseException.class, () -> { + ParserUtil.parseTimeSpanRecurringExpense(" "); + }); + } + + @Test + void parseTimeSpanRecurringExpense_validInput_success() throws ParseException { + assertEquals(RecurringExpenseType.DAILY, ParserUtil.parseTimeSpanRecurringExpense("day")); + assertEquals(RecurringExpenseType.WEEKLY, ParserUtil.parseTimeSpanRecurringExpense("week")); + assertEquals(RecurringExpenseType.MONTHLY, ParserUtil.parseTimeSpanRecurringExpense("month")); + assertEquals(RecurringExpenseType.YEARLY, ParserUtil.parseTimeSpanRecurringExpense("year")); + assertEquals(RecurringExpenseType.DAILY, ParserUtil.parseTimeSpanRecurringExpense("d")); + assertEquals(RecurringExpenseType.WEEKLY, ParserUtil.parseTimeSpanRecurringExpense("w")); + assertEquals(RecurringExpenseType.MONTHLY, ParserUtil.parseTimeSpanRecurringExpense("m")); + assertEquals(RecurringExpenseType.YEARLY, ParserUtil.parseTimeSpanRecurringExpense("y")); + } + + @Test + void parseTimeSpanRecurringExpense_invalidInput_throwsParseException() { + assertThrows(ParseException.class, () -> { + ParserUtil.parseTimeSpanRecurringExpense("wk"); + }); + assertThrows(ParseException.class, () -> { + ParserUtil.parseTimeSpanRecurringExpense("mon"); + }); + assertThrows(ParseException.class, () -> { + ParserUtil.parseTimeSpanRecurringExpense(""); + }); + assertThrows(ParseException.class, () -> { + ParserUtil.parseTimeSpanRecurringExpense(" "); + }); + assertThrows(ParseException.class, () -> { + ParserUtil.parseTimeSpanRecurringExpense("days"); + }); + } + + + @Test + void getDateByTimespan_validInput_success() { + LocalDate expectedWeekDate = LocalDate.now().with(DayOfWeek.MONDAY); + LocalDate expectedMonthDate = LocalDate.now().withDayOfMonth(1); + LocalDate expectedYearDate = LocalDate.now().withDayOfYear(1); + assertEquals(expectedWeekDate, ParserUtil.getDateByTimespan(ParserUtil.Timespan.WEEK)); + assertEquals(expectedMonthDate, ParserUtil.getDateByTimespan(ParserUtil.Timespan.MONTH)); + assertEquals(expectedYearDate, ParserUtil.getDateByTimespan(ParserUtil.Timespan.YEAR)); + } + + +} diff --git a/src/test/java/fasttrack/logic/parser/SetBudgetParserTest.java b/src/test/java/fasttrack/logic/parser/SetBudgetParserTest.java new file mode 100644 index 00000000000..c708af68082 --- /dev/null +++ b/src/test/java/fasttrack/logic/parser/SetBudgetParserTest.java @@ -0,0 +1,33 @@ +package fasttrack.logic.parser; + +import static fasttrack.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static fasttrack.logic.parser.CommandParserTestUtil.assertParseFailure; +import static fasttrack.logic.parser.CommandParserTestUtil.assertParseSuccess; + +import org.junit.jupiter.api.Test; + +import fasttrack.logic.commands.SetBudgetCommand; +import fasttrack.model.Budget; +import fasttrack.model.expense.Price; + +class SetBudgetParserTest { + + private final SetBudgetParser parser = new SetBudgetParser(); + + @Test + void parse_validArgs_returnsSetBudgetCommand() { + String input1 = " p/1000"; + SetBudgetCommand expected1 = new SetBudgetCommand(new Budget(1000)); + assertParseSuccess(parser, input1, expected1); + } + + @Test + void parse_invalidValue_throwsParseException() { + String input1 = " p/-1000"; + assertParseFailure(parser, input1, Price.MESSAGE_CONSTRAINTS); + + String input2 = " "; + assertParseFailure(parser, input2, + String.format(MESSAGE_INVALID_COMMAND_FORMAT, SetBudgetCommand.MESSAGE_USAGE)); + } +} diff --git a/src/test/java/fasttrack/logic/parser/add/AddCategoryCommandParserTest.java b/src/test/java/fasttrack/logic/parser/add/AddCategoryCommandParserTest.java new file mode 100644 index 00000000000..8b6f8f24863 --- /dev/null +++ b/src/test/java/fasttrack/logic/parser/add/AddCategoryCommandParserTest.java @@ -0,0 +1,35 @@ +package fasttrack.logic.parser.add; + +import static fasttrack.logic.commands.CommandTestUtil.CAT_APPLE; +import static fasttrack.logic.commands.CommandTestUtil.INVALID_CATEGORY_DESC; +import static fasttrack.logic.commands.CommandTestUtil.SUM_APPLE; +import static fasttrack.logic.parser.CommandParserTestUtil.assertParseFailure; +import static fasttrack.logic.parser.CommandParserTestUtil.assertParseSuccess; +import static fasttrack.testutil.TypicalCategories.FOOD; +import static fasttrack.testutil.TypicalCategories.FOOD_NO_SUMMARY; + +import org.junit.jupiter.api.Test; + +import fasttrack.logic.commands.add.AddCategoryCommand; +import fasttrack.model.category.Category; + +class AddCategoryCommandParserTest { + private final AddCategoryCommandParser parser = new AddCategoryCommandParser(); + + @Test + void parse_validArgs_returnsAddCategoryCommand() { + String input1 = CAT_APPLE; + AddCategoryCommand expectedCommand1 = new AddCategoryCommand(FOOD_NO_SUMMARY); + assertParseSuccess(parser, input1, expectedCommand1); + + String input2 = CAT_APPLE + SUM_APPLE; + AddCategoryCommand expectedCommand2 = new AddCategoryCommand(FOOD); + assertParseSuccess(parser, input2, expectedCommand2); + } + + @Test + void parse_invalidValue_throwsParseException() { + String input1 = INVALID_CATEGORY_DESC; + assertParseFailure(parser, input1, Category.MESSAGE_CONSTRAINTS); + } +} diff --git a/src/test/java/fasttrack/logic/parser/add/AddExpenseCommandParserTest.java b/src/test/java/fasttrack/logic/parser/add/AddExpenseCommandParserTest.java new file mode 100644 index 00000000000..c28df342116 --- /dev/null +++ b/src/test/java/fasttrack/logic/parser/add/AddExpenseCommandParserTest.java @@ -0,0 +1,56 @@ +package fasttrack.logic.parser.add; + +import static fasttrack.commons.core.Messages.MESSAGE_INVALID_DATE_FORMAT; +import static fasttrack.logic.commands.CommandTestUtil.AMT_APPLE; +import static fasttrack.logic.commands.CommandTestUtil.AMT_BANANA; +import static fasttrack.logic.commands.CommandTestUtil.CAT_APPLE; +import static fasttrack.logic.commands.CommandTestUtil.CAT_BANANA; +import static fasttrack.logic.commands.CommandTestUtil.DATE_APPLE; +import static fasttrack.logic.commands.CommandTestUtil.DATE_BANANA; +import static fasttrack.logic.commands.CommandTestUtil.DESC_APPLE; +import static fasttrack.logic.commands.CommandTestUtil.DESC_BANANA; +import static fasttrack.logic.commands.CommandTestUtil.INVALID_AMOUNT_DESC; +import static fasttrack.logic.commands.CommandTestUtil.INVALID_CATEGORY_DESC; +import static fasttrack.logic.commands.CommandTestUtil.INVALID_DATE_FORMAT_DESC; +import static fasttrack.logic.parser.CommandParserTestUtil.assertParseFailure; +import static fasttrack.logic.parser.CommandParserTestUtil.assertParseSuccess; +import static fasttrack.testutil.TypicalExpenses.APPLE; +import static fasttrack.testutil.TypicalExpenses.BANANA; + +import org.junit.jupiter.api.Test; + +import fasttrack.logic.commands.add.AddExpenseCommand; +import fasttrack.model.category.Category; +import fasttrack.model.expense.Price; + + +class AddExpenseCommandParserTest { + + private final AddExpenseCommandParser parser = new AddExpenseCommandParser(); + + @Test + void parse_validArgs_returnsAddExpenseCommand() { + String input1 = DESC_APPLE + AMT_APPLE + DATE_APPLE + CAT_APPLE; + AddExpenseCommand expectedCommand1 = new AddExpenseCommand(APPLE); + assertParseSuccess(parser, input1, expectedCommand1); + + String input2 = DESC_BANANA + AMT_BANANA + CAT_BANANA + DATE_BANANA; + AddExpenseCommand expectedCommand2 = new AddExpenseCommand(BANANA); + assertParseSuccess(parser, input2, expectedCommand2); + } + + @Test + void parse_invalidValue_throwsParseException() { + // invalid category name (with alphanumeric) + String input1 = DESC_APPLE + AMT_APPLE + DATE_APPLE + INVALID_CATEGORY_DESC; + assertParseFailure(parser, input1, Category.MESSAGE_CONSTRAINTS); + + // invalid date format + String input2 = DESC_APPLE + AMT_APPLE + INVALID_DATE_FORMAT_DESC + CAT_APPLE; + assertParseFailure(parser, input2, MESSAGE_INVALID_DATE_FORMAT); + + // invalid price + String input3 = DESC_APPLE + INVALID_AMOUNT_DESC + DATE_APPLE + CAT_APPLE; + assertParseFailure(parser, input3, Price.MESSAGE_CONSTRAINTS); + } +} diff --git a/src/test/java/fasttrack/logic/parser/add/AddRecurringExpenseCommandParserTest.java b/src/test/java/fasttrack/logic/parser/add/AddRecurringExpenseCommandParserTest.java new file mode 100644 index 00000000000..97475691ad7 --- /dev/null +++ b/src/test/java/fasttrack/logic/parser/add/AddRecurringExpenseCommandParserTest.java @@ -0,0 +1,122 @@ +package fasttrack.logic.parser.add; + +import static fasttrack.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static fasttrack.commons.core.Messages.MESSAGE_INVALID_DATE_FORMAT; +import static fasttrack.logic.commands.CommandTestUtil.AMT_APPLE; +import static fasttrack.logic.commands.CommandTestUtil.CAT_APPLE; +import static fasttrack.logic.commands.CommandTestUtil.DESC_APPLE; +import static fasttrack.logic.commands.CommandTestUtil.INVALID_AMOUNT_DESC; +import static fasttrack.logic.commands.CommandTestUtil.INVALID_CATEGORY_DESC; +import static fasttrack.logic.commands.CommandTestUtil.INVALID_DATE; +import static fasttrack.logic.commands.CommandTestUtil.INVALID_END_DATE; +import static fasttrack.logic.commands.CommandTestUtil.INVALID_INTERVAL; +import static fasttrack.logic.commands.CommandTestUtil.INVALID_START_DATE; +import static fasttrack.logic.commands.CommandTestUtil.VALID_END_DATE; +import static fasttrack.logic.commands.CommandTestUtil.VALID_INTERVAL_DAY; +import static fasttrack.logic.commands.CommandTestUtil.VALID_INTERVAL_MONTH; +import static fasttrack.logic.commands.CommandTestUtil.VALID_INTERVAL_WEEK; +import static fasttrack.logic.commands.CommandTestUtil.VALID_INTERVAL_YEAR; +import static fasttrack.logic.commands.CommandTestUtil.VALID_START_DATE; +import static fasttrack.logic.parser.CommandParserTestUtil.assertParseFailure; +import static fasttrack.logic.parser.CommandParserTestUtil.assertParseSuccess; +import static fasttrack.testutil.TypicalRecurringExpenseManagers.RECUR_APPLE_DAY; +import static fasttrack.testutil.TypicalRecurringExpenseManagers.RECUR_APPLE_DAY_WITH_END; +import static fasttrack.testutil.TypicalRecurringExpenseManagers.RECUR_APPLE_MONTH; +import static fasttrack.testutil.TypicalRecurringExpenseManagers.RECUR_APPLE_MONTH_WITH_END; +import static fasttrack.testutil.TypicalRecurringExpenseManagers.RECUR_APPLE_WEEK; +import static fasttrack.testutil.TypicalRecurringExpenseManagers.RECUR_APPLE_WEEK_WITH_END; +import static fasttrack.testutil.TypicalRecurringExpenseManagers.RECUR_APPLE_YEAR; +import static fasttrack.testutil.TypicalRecurringExpenseManagers.RECUR_APPLE_YEAR_WITH_END; + +import org.junit.jupiter.api.Test; + +import fasttrack.logic.commands.add.AddRecurringExpenseCommand; +import fasttrack.model.category.Category; +import fasttrack.model.expense.Price; + +class AddRecurringExpenseCommandParserTest { + private final AddRecurringExpenseCommandParser parser = new AddRecurringExpenseCommandParser(); + + @Test + void parse_validArgs_returnsAddRecurringExpenseCommand() { + String input1 = DESC_APPLE + AMT_APPLE + CAT_APPLE + VALID_INTERVAL_DAY + VALID_START_DATE; + AddRecurringExpenseCommand expected1 = new AddRecurringExpenseCommand(RECUR_APPLE_DAY); + assertParseSuccess(parser, input1, expected1); + + String input2 = DESC_APPLE + AMT_APPLE + CAT_APPLE + VALID_INTERVAL_WEEK + VALID_START_DATE; + AddRecurringExpenseCommand expected2 = new AddRecurringExpenseCommand(RECUR_APPLE_WEEK); + assertParseSuccess(parser, input2, expected2); + + String input3 = DESC_APPLE + AMT_APPLE + CAT_APPLE + VALID_INTERVAL_MONTH + VALID_START_DATE; + AddRecurringExpenseCommand expected3 = new AddRecurringExpenseCommand(RECUR_APPLE_MONTH); + assertParseSuccess(parser, input3, expected3); + + String input4 = DESC_APPLE + AMT_APPLE + CAT_APPLE + VALID_INTERVAL_YEAR + VALID_START_DATE; + AddRecurringExpenseCommand expected4 = new AddRecurringExpenseCommand(RECUR_APPLE_YEAR); + assertParseSuccess(parser, input4, expected4); + + String input5 = DESC_APPLE + AMT_APPLE + CAT_APPLE + VALID_INTERVAL_DAY + VALID_START_DATE + VALID_END_DATE; + AddRecurringExpenseCommand expected5 = new AddRecurringExpenseCommand(RECUR_APPLE_DAY_WITH_END); + assertParseSuccess(parser, input5, expected5); + + String input6 = DESC_APPLE + AMT_APPLE + CAT_APPLE + VALID_INTERVAL_WEEK + VALID_START_DATE + VALID_END_DATE; + AddRecurringExpenseCommand expected6 = new AddRecurringExpenseCommand(RECUR_APPLE_WEEK_WITH_END); + assertParseSuccess(parser, input6, expected6); + + String input7 = DESC_APPLE + AMT_APPLE + CAT_APPLE + VALID_INTERVAL_MONTH + VALID_START_DATE + VALID_END_DATE; + AddRecurringExpenseCommand expected7 = new AddRecurringExpenseCommand(RECUR_APPLE_MONTH_WITH_END); + assertParseSuccess(parser, input7, expected7); + + String input8 = DESC_APPLE + AMT_APPLE + CAT_APPLE + VALID_INTERVAL_YEAR + VALID_START_DATE + VALID_END_DATE; + AddRecurringExpenseCommand expected8 = new AddRecurringExpenseCommand(RECUR_APPLE_YEAR_WITH_END); + assertParseSuccess(parser, input8, expected8); + } + + @Test + void parse_invalidValue_throwsParseException() { + // Invalid category name + String input1 = DESC_APPLE + AMT_APPLE + INVALID_CATEGORY_DESC + VALID_INTERVAL_DAY + VALID_START_DATE; + assertParseFailure(parser, input1, Category.MESSAGE_CONSTRAINTS); + + // invalid price + String input2 = DESC_APPLE + INVALID_AMOUNT_DESC + CAT_APPLE + VALID_INTERVAL_DAY + VALID_START_DATE; + assertParseFailure(parser, input2, Price.MESSAGE_CONSTRAINTS); + + // invalid start date + String input3 = DESC_APPLE + AMT_APPLE + CAT_APPLE + VALID_INTERVAL_DAY + INVALID_DATE; + assertParseFailure(parser, input3, MESSAGE_INVALID_DATE_FORMAT); + + // End date after start date + String input4 = DESC_APPLE + AMT_APPLE + CAT_APPLE + VALID_INTERVAL_DAY + INVALID_START_DATE + INVALID_END_DATE; + assertParseFailure(parser, input4, "End date provided is earlier than start date."); + + // Invalid interval + String input5 = DESC_APPLE + AMT_APPLE + CAT_APPLE + INVALID_INTERVAL + VALID_START_DATE; + assertParseFailure(parser, input5, "Not a valid date format (day, week, month, year)"); + + // Missing name + String input6 = AMT_APPLE + CAT_APPLE + VALID_INTERVAL_DAY + VALID_START_DATE; + assertParseFailure(parser, input6, String.format(MESSAGE_INVALID_COMMAND_FORMAT, + AddRecurringExpenseCommand.MESSAGE_USAGE)); + + // Missing category + String input7 = DESC_APPLE + AMT_APPLE + VALID_INTERVAL_DAY + VALID_START_DATE; + assertParseFailure(parser, input6, String.format(MESSAGE_INVALID_COMMAND_FORMAT, + AddRecurringExpenseCommand.MESSAGE_USAGE)); + + // Missing price + String input8 = DESC_APPLE + CAT_APPLE + VALID_INTERVAL_DAY + VALID_START_DATE; + assertParseFailure(parser, input8, String.format(MESSAGE_INVALID_COMMAND_FORMAT, + AddRecurringExpenseCommand.MESSAGE_USAGE)); + + // Missing interval + String input9 = DESC_APPLE + AMT_APPLE + CAT_APPLE + VALID_START_DATE; + assertParseFailure(parser, input9, String.format(MESSAGE_INVALID_COMMAND_FORMAT, + AddRecurringExpenseCommand.MESSAGE_USAGE)); + + // Missing start date + String input10 = DESC_APPLE + AMT_APPLE + CAT_APPLE + VALID_INTERVAL_DAY; + assertParseFailure(parser, input10, String.format(MESSAGE_INVALID_COMMAND_FORMAT, + AddRecurringExpenseCommand.MESSAGE_USAGE)); + } +} diff --git a/src/test/java/fasttrack/logic/parser/delete/DeleteCategoryCommandParserTest.java b/src/test/java/fasttrack/logic/parser/delete/DeleteCategoryCommandParserTest.java new file mode 100644 index 00000000000..81e2c968f59 --- /dev/null +++ b/src/test/java/fasttrack/logic/parser/delete/DeleteCategoryCommandParserTest.java @@ -0,0 +1,34 @@ +package fasttrack.logic.parser.delete; + +import static fasttrack.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static fasttrack.logic.parser.CommandParserTestUtil.assertParseFailure; +import static fasttrack.logic.parser.CommandParserTestUtil.assertParseSuccess; +import static fasttrack.testutil.TypicalIndexes.INDEX_FIRST_PERSON; + +import org.junit.jupiter.api.Test; + +import fasttrack.logic.commands.delete.DeleteCategoryCommand; + +class DeleteCategoryCommandParserTest { + private final DeleteCategoryCommandParser parser = new DeleteCategoryCommandParser(); + + @Test + void parse_validArgs_returnsDeleteCategoryCommand() { + String input1 = "1"; + DeleteCategoryCommand expected1 = new DeleteCategoryCommand(INDEX_FIRST_PERSON); + assertParseSuccess(parser, input1, expected1); + } + + @Test + void parse_invalidValue_throwsParseException() { + // Invalid index + String input1 = "0"; + assertParseFailure(parser, input1, String.format(MESSAGE_INVALID_COMMAND_FORMAT, + DeleteCategoryCommand.MESSAGE_USAGE)); + + // Missing index + String input2 = ""; + assertParseFailure(parser, input2, String.format(MESSAGE_INVALID_COMMAND_FORMAT, + DeleteCategoryCommand.MESSAGE_USAGE)); + } +} diff --git a/src/test/java/fasttrack/logic/parser/delete/DeleteRecurringExpenseCommandParserTest.java b/src/test/java/fasttrack/logic/parser/delete/DeleteRecurringExpenseCommandParserTest.java new file mode 100644 index 00000000000..ce8e2fff669 --- /dev/null +++ b/src/test/java/fasttrack/logic/parser/delete/DeleteRecurringExpenseCommandParserTest.java @@ -0,0 +1,34 @@ +package fasttrack.logic.parser.delete; + +import static fasttrack.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static fasttrack.logic.parser.CommandParserTestUtil.assertParseFailure; +import static fasttrack.logic.parser.CommandParserTestUtil.assertParseSuccess; +import static fasttrack.testutil.TypicalIndexes.INDEX_FIRST_PERSON; + +import org.junit.jupiter.api.Test; + +import fasttrack.logic.commands.delete.DeleteRecurringExpenseCommand; + +class DeleteRecurringExpenseCommandParserTest { + private final DeleteRecurringExpenseCommandParser parser = new DeleteRecurringExpenseCommandParser(); + + @Test + void parse_validArgs_returnsDeleteRecurringExpenseCommand() { + String input1 = "1"; + DeleteRecurringExpenseCommand expected1 = new DeleteRecurringExpenseCommand(INDEX_FIRST_PERSON); + assertParseSuccess(parser, input1, expected1); + } + + @Test + void parse_invalidValue_throwsParseException() { + // Invalid index + String input1 = "0"; + assertParseFailure(parser, input1, String.format(MESSAGE_INVALID_COMMAND_FORMAT, + DeleteRecurringExpenseCommand.MESSAGE_USAGE)); + + // Missing index + String input2 = ""; + assertParseFailure(parser, input2, String.format(MESSAGE_INVALID_COMMAND_FORMAT, + DeleteRecurringExpenseCommand.MESSAGE_USAGE)); + } +} diff --git a/src/test/java/fasttrack/logic/parser/edit/EditCategoryCommandParserTest.java b/src/test/java/fasttrack/logic/parser/edit/EditCategoryCommandParserTest.java new file mode 100644 index 00000000000..43013c404d4 --- /dev/null +++ b/src/test/java/fasttrack/logic/parser/edit/EditCategoryCommandParserTest.java @@ -0,0 +1,60 @@ +package fasttrack.logic.parser.edit; + +import static fasttrack.logic.parser.CommandParserTestUtil.assertParseFailure; +import static fasttrack.logic.parser.CommandParserTestUtil.assertParseSuccess; + +import org.junit.jupiter.api.Test; + +import fasttrack.commons.core.index.Index; +import fasttrack.logic.commands.edit.EditCategoryCommand; +import fasttrack.logic.parser.ParserUtil; +import fasttrack.logic.parser.exceptions.ParseException; + +public class EditCategoryCommandParserTest { + + public static final String MESSAGE_INVALID_INDEX = "The index provided is invalid."; + private final EditCategoryCommandParser parser = new EditCategoryCommandParser(); + + @Test + public void parse_noIndex_throwsParseException() { + assertParseFailure(parser, "", MESSAGE_INVALID_INDEX); + } + + @Test + public void parse_noArgumentsExceptIndex() throws ParseException { + Index toUseForExpected = ParserUtil.parseIndex("1"); + EditCategoryCommand expectedCommand = new EditCategoryCommand(toUseForExpected, null, null); + assertParseSuccess(parser, "1", expectedCommand); + } + + @Test + public void parse_editCategoryNameParsed_success() throws ParseException { + Index toUseForExpected = ParserUtil.parseIndex("1"); + EditCategoryCommand expectedCommand = new EditCategoryCommand(toUseForExpected, "changedName", null); + assertParseSuccess(parser, "1 c/changedName", expectedCommand); + } + + @Test + public void parse_editCategorySummary_success() throws ParseException { + Index toUseForExpected = ParserUtil.parseIndex("1"); + EditCategoryCommand expectedCommand = new EditCategoryCommand(toUseForExpected, null, "changedSummary"); + assertParseSuccess(parser, "1 s/changedSummary", expectedCommand); + } + + @Test + public void parse_editBothNameSummary_success() throws ParseException { + Index toUseForExpected = ParserUtil.parseIndex("1"); + EditCategoryCommand expectedCommand = new EditCategoryCommand(toUseForExpected, "changedName", + "changedSummary"); + assertParseSuccess(parser, "1 c/changedName s/changedSummary", expectedCommand); + } + + @Test + public void parse_editBothNameSummarySwapPos_success() throws ParseException { + Index toUseForExpected = ParserUtil.parseIndex("1"); + EditCategoryCommand expectedCommand = new EditCategoryCommand(toUseForExpected, "changedName", + "changedSummary"); + assertParseSuccess(parser, "1 s/changedSummary c/changedName", expectedCommand); + } + +} diff --git a/src/test/java/fasttrack/logic/parser/edit/EditExpenseCommandParserTest.java b/src/test/java/fasttrack/logic/parser/edit/EditExpenseCommandParserTest.java new file mode 100644 index 00000000000..b856ab36b66 --- /dev/null +++ b/src/test/java/fasttrack/logic/parser/edit/EditExpenseCommandParserTest.java @@ -0,0 +1,59 @@ +package fasttrack.logic.parser.edit; + +import static fasttrack.logic.parser.CommandParserTestUtil.assertParseFailure; +import static fasttrack.logic.parser.CommandParserTestUtil.assertParseSuccess; + +import org.junit.jupiter.api.Test; + +import fasttrack.commons.core.index.Index; +import fasttrack.logic.commands.edit.EditExpenseCommand; +import fasttrack.logic.parser.ParserUtil; +import fasttrack.logic.parser.exceptions.ParseException; + +public class EditExpenseCommandParserTest { + + public static final String MESSAGE_INVALID_INDEX = "The index provided is invalid."; + private final EditExpenseCommandParser parser = new EditExpenseCommandParser(); + + @Test + public void parse_noIndex_throwsParseException() { + assertParseFailure(parser, "", MESSAGE_INVALID_INDEX); + } + + @Test + public void parse_noArgumentsExceptIndex_success() throws ParseException { + Index toUseForExpected = ParserUtil.parseIndex("1"); + EditExpenseCommand expectedCommand = new EditExpenseCommand(toUseForExpected, null, + null, null, null); + assertParseSuccess(parser, "1", expectedCommand); + } + + @Test + public void parse_onlyValidPriceChange_success() throws ParseException { + Index toUseForExpected = ParserUtil.parseIndex("1"); + EditExpenseCommand expectedCommand = new EditExpenseCommand(toUseForExpected, null, + 4.50, null, null); + assertParseSuccess(parser, "1 p/4.50", expectedCommand); + } + + @Test + public void parse_onlyValidCategoryChange_success() throws ParseException { + Index toUseForExpected = ParserUtil.parseIndex("1"); + EditExpenseCommand expectedCommand = new EditExpenseCommand(toUseForExpected, null, + null, null, "NewCategory"); + assertParseSuccess(parser, "1 c/NewCategory", expectedCommand); + } + + @Test + public void parse_onlyValidDateChange_success() throws ParseException { + Index toUseForExpected = ParserUtil.parseIndex("1"); + EditExpenseCommand expectedCommand = new EditExpenseCommand(toUseForExpected, null, + null, ParserUtil.parseDate("24/3/23"), null); + assertParseSuccess(parser, "1 d/24/3/23", expectedCommand); + } + + @Test + public void parse_invalidDateChange_success() throws ParseException { + assertParseFailure(parser, "1 d/2023/03/21", "Invalid date format! Please use DD/MM/YY format!"); + } +} diff --git a/src/test/java/fasttrack/logic/parser/edit/EditRecurringExpenseManagerCommandParserTest.java b/src/test/java/fasttrack/logic/parser/edit/EditRecurringExpenseManagerCommandParserTest.java new file mode 100644 index 00000000000..cea74396bf7 --- /dev/null +++ b/src/test/java/fasttrack/logic/parser/edit/EditRecurringExpenseManagerCommandParserTest.java @@ -0,0 +1,59 @@ +package fasttrack.logic.parser.edit; + +import static fasttrack.logic.parser.CommandParserTestUtil.assertParseFailure; +import static fasttrack.logic.parser.CommandParserTestUtil.assertParseSuccess; + +import org.junit.jupiter.api.Test; + +import fasttrack.commons.core.index.Index; +import fasttrack.logic.commands.edit.EditRecurringExpenseManagerCommand; +import fasttrack.logic.parser.ParserUtil; +import fasttrack.logic.parser.exceptions.ParseException; + +public class EditRecurringExpenseManagerCommandParserTest { + + public static final String MESSAGE_INVALID_INDEX = "The index provided is invalid."; + private final EditRecurringExpenseManagerCommandParser parser = new EditRecurringExpenseManagerCommandParser(); + @Test + public void parse_noIndex_throwsParseException() { + assertParseFailure(parser, "", MESSAGE_INVALID_INDEX); + } + + @Test + public void parse_noArgumentsExceptIndex_success() throws ParseException { + Index toUseForExpected = ParserUtil.parseIndex("1"); + EditRecurringExpenseManagerCommand expectedCommand = new EditRecurringExpenseManagerCommand(toUseForExpected, + null, null, null, null, null); + assertParseSuccess(parser, "1", expectedCommand); + } + + @Test + public void parse_onlyValidPriceChange_success() throws ParseException { + Index toUseForExpected = ParserUtil.parseIndex("1"); + EditRecurringExpenseManagerCommand expectedCommand = new EditRecurringExpenseManagerCommand(toUseForExpected, + null, 4.50, null, null, null); + assertParseSuccess(parser, "1 p/4.50", expectedCommand); + } + + @Test + public void parse_onlyValidCategoryChange_success() throws ParseException { + Index toUseForExpected = ParserUtil.parseIndex("1"); + EditRecurringExpenseManagerCommand expectedCommand = new EditRecurringExpenseManagerCommand(toUseForExpected, + null, null, "NewCategory", null, null); + assertParseSuccess(parser, "1 c/NewCategory", expectedCommand); + } + + @Test + public void parse_onlyValidEndDateChange_success() throws ParseException { + Index toUseForExpected = ParserUtil.parseIndex("1"); + EditRecurringExpenseManagerCommand expectedCommand = new EditRecurringExpenseManagerCommand(toUseForExpected, + null, null, null, null, + ParserUtil.parseDate("24/3/23")); + assertParseSuccess(parser, "1 ed/24/3/23", expectedCommand); + } + + @Test + public void parse_invalidEndDateChange_success() throws ParseException { + assertParseFailure(parser, "1 ed/2023/03/21", "Invalid date format! Please use DD/MM/YY format!"); + } +} diff --git a/src/test/java/fasttrack/model/AnalyticModelManagerTest.java b/src/test/java/fasttrack/model/AnalyticModelManagerTest.java new file mode 100644 index 00000000000..3b1debe182c --- /dev/null +++ b/src/test/java/fasttrack/model/AnalyticModelManagerTest.java @@ -0,0 +1,126 @@ +package fasttrack.model; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.fail; + +import java.time.LocalDate; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import fasttrack.model.util.AnalyticsType; +import fasttrack.testutil.TypicalExpenses; +import javafx.beans.property.DoubleProperty; + + +class AnalyticModelManagerTest { + + private LocalDate testDate; + private AnalyticModel analyticModel; + + @BeforeEach + public void setUp() { + ReadOnlyExpenseTracker expenseTracker = TypicalExpenses.getTypicalExpenseTracker(); + testDate = LocalDate.of(2023, 3, 3); + analyticModel = new AnalyticModelManager(expenseTracker, testDate); + } + + @Test + public void getMonthlySpent_monthContainsExpenses_isValidCalculation() { + assertEquals(27.7, analyticModel.getMonthlySpent().get()); + } + + @Test + public void getMonthlyRemaining_monthContainsExpenses_isValidCalculation() { + assertEquals((1000 - 27.7), analyticModel.getMonthlyRemaining().get()); + } + + @Test + public void getWeeklySpent_weekContainsExpenses_isValidCalculation() { + assertEquals(2.7, analyticModel.getWeeklySpent().get()); + } + + @Test + public void getWeeklyRemaining_weekContainsExpenses_isValidCalculation() { + assertEquals(((1000 / 4) - 2.7), analyticModel.getWeeklyRemaining().get()); + } + + @Test + public void getWeeklyChange_weekContainsExpenses_isValidCalculation() { + // previous week has no expenses + assertEquals(0, analyticModel.getWeeklyChange().get()); + } + + @Test + public void getMonthlyChange_monthContainsExpenses_isValidCalculation() { + assertEquals(((27.7 - 1000) / 1000) * 100, analyticModel.getMonthlyChange().get()); + } + + @Test + public void getTotalSpent_containsExpenses_isValidCalculation() { + assertEquals(1031.7, analyticModel.getTotalSpent().get()); + } + + @Test + public void getBudgetPercentage_containsExpenses_isValidCalculation() { + assertEquals(2.77, analyticModel.getBudgetPercentage().get()); + } + + @Test + public void testGetAnalyticsData_allAnalyticsTypes_givesValidCalculations() { + try { + for (AnalyticsType type : AnalyticsType.values()) { + DoubleProperty result = analyticModel.getAnalyticsData(type); + switch (type) { + case MONTHLY_SPENT: + assertEquals(27.7, result.get()); + break; + case MONTHLY_REMAINING: + assertEquals((1000 - 27.7), result.get()); + break; + case WEEKLY_SPENT: + assertEquals(2.7, result.get()); + break; + case WEEKLY_REMAINING: + assertEquals(((1000 / 4) - 2.7), result.get()); + break; + case WEEKLY_CHANGE: + assertEquals(0, result.get()); + break; + case MONTHLY_CHANGE: + assertEquals(((27.7 - 1000) / 1000) * 100, result.get()); + break; + case TOTAL_SPENT: + assertEquals(1031.7, result.get()); + break; + case BUDGET_PERCENTAGE: + assertEquals(2.77, result.get()); + break; + default: + fail("Unexpected analytics type was provided"); + } + } + } catch (IllegalArgumentException e) { + fail("Unexpected exception occurred"); + } + } + + @Test + public void equals() { + ExpenseTracker expenseTracker = TypicalExpenses.getTypicalExpenseTracker(); + ExpenseTracker differentExpenseTracker = new ExpenseTracker(); + AnalyticModelManager analyticModelManager = new AnalyticModelManager(expenseTracker, testDate); + AnalyticModelManager analyticModelManagerCopy = new AnalyticModelManager(expenseTracker, testDate); + // same values -> returns true + assertEquals(analyticModelManager, analyticModelManagerCopy); + // same object -> returns true + assertEquals(analyticModelManager, analyticModelManager); + // null -> returns false + assertNotEquals(null, analyticModelManager); + // different types -> returns false + assertNotEquals(5, analyticModelManager); + // different expense tracker -> returns false + assertNotEquals(analyticModelManager, new AnalyticModelManager(differentExpenseTracker)); + } +} diff --git a/src/test/java/seedu/address/model/UserPrefsTest.java b/src/test/java/fasttrack/model/UserPrefsTest.java similarity index 80% rename from src/test/java/seedu/address/model/UserPrefsTest.java rename to src/test/java/fasttrack/model/UserPrefsTest.java index b1307a70d52..d101930f436 100644 --- a/src/test/java/seedu/address/model/UserPrefsTest.java +++ b/src/test/java/fasttrack/model/UserPrefsTest.java @@ -1,6 +1,6 @@ -package seedu.address.model; +package fasttrack.model; -import static seedu.address.testutil.Assert.assertThrows; +import static fasttrack.testutil.Assert.assertThrows; import org.junit.jupiter.api.Test; @@ -15,7 +15,7 @@ public void setGuiSettings_nullGuiSettings_throwsNullPointerException() { @Test public void setAddressBookFilePath_nullPath_throwsNullPointerException() { UserPrefs userPrefs = new UserPrefs(); - assertThrows(NullPointerException.class, () -> userPrefs.setAddressBookFilePath(null)); + assertThrows(NullPointerException.class, () -> userPrefs.setExpenseTrackerFilePath(null)); } } diff --git a/src/test/java/fasttrack/model/category/CategoryTest.java b/src/test/java/fasttrack/model/category/CategoryTest.java new file mode 100644 index 00000000000..6f2b23ada3d --- /dev/null +++ b/src/test/java/fasttrack/model/category/CategoryTest.java @@ -0,0 +1,5 @@ +package fasttrack.model.category; + +public class CategoryTest { + +} diff --git a/src/test/java/fasttrack/model/category/MiscellaneousCategoryTest.java b/src/test/java/fasttrack/model/category/MiscellaneousCategoryTest.java new file mode 100644 index 00000000000..772498e1e4b --- /dev/null +++ b/src/test/java/fasttrack/model/category/MiscellaneousCategoryTest.java @@ -0,0 +1,34 @@ +package fasttrack.model.category; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +public class MiscellaneousCategoryTest { + private final MiscellaneousCategory miscellaneousCategory = new MiscellaneousCategory(); + + @Test + public void toStringTest() { + assertEquals("Miscellaneous", miscellaneousCategory.toString()); + } + + @Test + public void getCategoryNameTest() { + assertEquals("Misc", miscellaneousCategory.getCategoryName()); + } + + @Test + public void getSummaryTest() { + assertEquals("Placeholder Description", miscellaneousCategory.getSummary()); + } + + @Test + public void equalsTest() { + assertEquals(miscellaneousCategory, miscellaneousCategory); + } + + @Test + public void hashCodeTest() { + assertEquals(miscellaneousCategory.hashCode(), miscellaneousCategory.hashCode()); + } +} diff --git a/src/test/java/fasttrack/model/category/UserDefinedCategoryTest.java b/src/test/java/fasttrack/model/category/UserDefinedCategoryTest.java new file mode 100644 index 00000000000..abc0b964b57 --- /dev/null +++ b/src/test/java/fasttrack/model/category/UserDefinedCategoryTest.java @@ -0,0 +1,29 @@ +package fasttrack.model.category; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +public class UserDefinedCategoryTest { + private final UserDefinedCategory userDefinedCategory = new UserDefinedCategory("test", "description"); + + @Test + public void toStringTest() { + assertEquals("test", userDefinedCategory.toString()); + } + + @Test + public void getDescriptionTest() { + assertEquals("description", userDefinedCategory.getSummary()); + } + + @Test + public void equalsTest() { + assertEquals(userDefinedCategory, new UserDefinedCategory("test", "description")); + } + + @Test + public void hashCodeTest() { + assertEquals(userDefinedCategory.hashCode(), new UserDefinedCategory("test", "description").hashCode()); + } +} diff --git a/src/test/java/fasttrack/model/expense/ExpenseContainsKeywordsPredicateTest.java b/src/test/java/fasttrack/model/expense/ExpenseContainsKeywordsPredicateTest.java new file mode 100644 index 00000000000..d5ae0e8eaf5 --- /dev/null +++ b/src/test/java/fasttrack/model/expense/ExpenseContainsKeywordsPredicateTest.java @@ -0,0 +1,87 @@ +package fasttrack.model.expense; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.LocalDate; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import fasttrack.model.category.MiscellaneousCategory; + + +public class ExpenseContainsKeywordsPredicateTest { + + @Test + public void equals() { + List firstPredicateKeywordList = Collections.singletonList("first"); + List secondPredicateKeywordList = Arrays.asList("first", "second"); + + ExpenseContainsKeywordsPredicate firstPredicate = + new ExpenseContainsKeywordsPredicate(firstPredicateKeywordList); + ExpenseContainsKeywordsPredicate secondPredicate = + new ExpenseContainsKeywordsPredicate(secondPredicateKeywordList); + + // same object -> returns true + assertTrue(firstPredicate.equals(firstPredicate)); + + // same values -> returns true + ExpenseContainsKeywordsPredicate firstPredicateCopy = + new ExpenseContainsKeywordsPredicate(firstPredicateKeywordList); + assertTrue(firstPredicate.equals(firstPredicateCopy)); + + // different types -> returns false + assertFalse(firstPredicate.equals(1)); + + // null -> returns false + assertFalse(firstPredicate.equals(null)); + + // different person -> returns false + assertFalse(firstPredicate.equals(secondPredicate)); + } + + @Test + public void test_expenseContainsKeywords_returnsTrue() { + // One keyword + ExpenseContainsKeywordsPredicate predicate = + new ExpenseContainsKeywordsPredicate(Collections.singletonList("Apple")); + assertTrue(predicate.test( + new Expense("Apple", 1.50, LocalDate.now(), new MiscellaneousCategory()))); + + // Multiple keywords + predicate = new ExpenseContainsKeywordsPredicate(Arrays.asList("Fuji", "Apple")); + assertTrue(predicate.test( + new Expense("Fuji Apple", 1.50, LocalDate.now(), new MiscellaneousCategory()))); + + // Only one matching keyword + predicate = new ExpenseContainsKeywordsPredicate(Arrays.asList("Orange", "Apple")); + assertTrue(predicate.test( + new Expense("Fuji Apple", 1.50, LocalDate.now(), new MiscellaneousCategory()))); + + // Mixed-case keywords + predicate = new ExpenseContainsKeywordsPredicate(Arrays.asList("fUjI", "aPPle")); + assertTrue(predicate.test( + new Expense("Fuji Apple", 1.50, LocalDate.now(), new MiscellaneousCategory()))); + } + + @Test + public void test_nameDoesNotContainKeywords_returnsFalse() { + // Zero keywords + ExpenseContainsKeywordsPredicate predicate = new ExpenseContainsKeywordsPredicate(Collections.emptyList()); + assertFalse(predicate.test( + new Expense("Fuji Apple", 1.50, LocalDate.now(), new MiscellaneousCategory()))); + + // Non-matching keyword + predicate = new ExpenseContainsKeywordsPredicate(Arrays.asList("Orange")); + assertFalse(predicate.test( + new Expense("Fuji Apple", 1.50, LocalDate.now(), new MiscellaneousCategory()))); + + // Keywords match price and category, but does not match name + predicate = new ExpenseContainsKeywordsPredicate(Arrays.asList("1.50", "Miscellaneous")); + assertFalse(predicate.test( + new Expense("Fuji Apple", 1.50, LocalDate.now(), new MiscellaneousCategory()))); + } +} diff --git a/src/test/java/fasttrack/model/expense/ExpenseInCategoryPredicateTest.java b/src/test/java/fasttrack/model/expense/ExpenseInCategoryPredicateTest.java new file mode 100644 index 00000000000..0ee2bdd6d9c --- /dev/null +++ b/src/test/java/fasttrack/model/expense/ExpenseInCategoryPredicateTest.java @@ -0,0 +1,77 @@ +package fasttrack.model.expense; + +import static fasttrack.testutil.TypicalCategories.FOOD; +import static fasttrack.testutil.TypicalCategories.TECH; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.LocalDate; + +import org.junit.jupiter.api.Test; + +import fasttrack.model.category.Category; +import fasttrack.model.category.MiscellaneousCategory; +import fasttrack.model.category.UserDefinedCategory; + +public class ExpenseInCategoryPredicateTest { + + @Test + public void equals() { + + ExpenseInCategoryPredicate firstPredicate = new ExpenseInCategoryPredicate(FOOD); + ExpenseInCategoryPredicate secondPredicate = new ExpenseInCategoryPredicate(TECH); + + // same object -> returns true + assertTrue(firstPredicate.equals(firstPredicate)); + + // same values -> returns true + ExpenseInCategoryPredicate firstPredicateCopy = new ExpenseInCategoryPredicate(FOOD); + assertTrue(firstPredicate.equals(firstPredicateCopy)); + + // different types -> returns false + assertFalse(firstPredicate.equals(1)); + + // null -> returns false + assertFalse(firstPredicate.equals(null)); + + // different person -> returns false + assertFalse(firstPredicate.equals(secondPredicate)); + } + + @Test + public void test_expenseInCategory_returnsTrue() { + Category category1 = new UserDefinedCategory("Category1", "Description1"); + Category miscCat = new MiscellaneousCategory(); + + ExpenseInCategoryPredicate predicate = new ExpenseInCategoryPredicate(category1); + assertTrue(predicate.test( + new Expense("Apple", 1.5, LocalDate.of(2023, 3, 15), category1))); + + predicate = new ExpenseInCategoryPredicate(miscCat); + assertTrue(predicate.test( + new Expense("Hello", 1.0, LocalDate.now(), miscCat) + )); + } + + @Test + public void test_expenseNotInCategory_returnsFalse() { + Category category1 = new UserDefinedCategory("Category1", "Description1"); + Category category2 = new UserDefinedCategory("Category2", "Description2"); + Category miscCat = new MiscellaneousCategory(); + + Expense expense = new Expense("test", 1, LocalDate.EPOCH, category1); + + // Wrong Category + ExpenseInCategoryPredicate predicate = new ExpenseInCategoryPredicate(category2); + assertFalse(predicate.test(expense)); + + expense = new Expense("test2", 2, LocalDate.EPOCH, miscCat); + assertFalse(predicate.test(expense)); + + predicate = new ExpenseInCategoryPredicate(miscCat); + expense = new Expense("test3", 3, LocalDate.now(), category1); + assertFalse(predicate.test(expense)); + + + } +} diff --git a/src/test/java/fasttrack/model/expense/ExpenseInTimespanPredicateTest.java b/src/test/java/fasttrack/model/expense/ExpenseInTimespanPredicateTest.java new file mode 100644 index 00000000000..183ab68bd39 --- /dev/null +++ b/src/test/java/fasttrack/model/expense/ExpenseInTimespanPredicateTest.java @@ -0,0 +1,72 @@ +package fasttrack.model.expense; + +import static fasttrack.logic.parser.ParserUtil.Timespan.MONTH; +import static fasttrack.logic.parser.ParserUtil.Timespan.WEEK; +import static fasttrack.testutil.TypicalCategories.MISCCAT; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.LocalDate; + +import org.junit.jupiter.api.Test; + +import fasttrack.model.category.Category; +import fasttrack.model.category.MiscellaneousCategory; +import fasttrack.model.category.UserDefinedCategory; + +public class ExpenseInTimespanPredicateTest { + + @Test + public void equals() { + ExpenseInTimespanPredicate firstPredicate = new ExpenseInTimespanPredicate(WEEK); + ExpenseInTimespanPredicate secondPredicate = new ExpenseInTimespanPredicate(MONTH); + + // same object -> returns true + assertTrue(firstPredicate.equals(firstPredicate)); + + // same values -> returns true + ExpenseInTimespanPredicate firstPredicateCopy = new ExpenseInTimespanPredicate(WEEK); + assertTrue(firstPredicate.equals(firstPredicateCopy)); + + // null -> returns false + assertFalse(firstPredicate.equals(null)); + + // different person -> returns false + assertFalse(firstPredicate.equals(secondPredicate)); + } + + @Test + public void test_expenseInTimespan_returnsTrue() { + LocalDate date = LocalDate.now(); + + ExpenseInTimespanPredicate predicate = + new ExpenseInTimespanPredicate(WEEK); + assertTrue(predicate.test( + new Expense("Apple", 1.5, date, MISCCAT))); + + predicate = new ExpenseInTimespanPredicate(MONTH); + assertTrue(predicate.test( + new Expense("Hello", 1.0, date.withDayOfMonth(1), MISCCAT) + )); + } + + @Test + public void test_expenseNotInTimespan_returnsFalse() { + Category category1 = new UserDefinedCategory("Category1", "Description1"); + Category category2 = new UserDefinedCategory("Category2", "Description2"); + Category miscCat = new MiscellaneousCategory(); + + + Expense expense = new Expense("test", 1, LocalDate.of(2023, 1, 1), category1); + + // Wrong Category + ExpenseInTimespanPredicate predicate = + new ExpenseInTimespanPredicate(MONTH); + assertFalse(predicate.test(expense)); + + expense = new Expense("test2", 2, LocalDate.of(2023, 1, 31), miscCat); + assertFalse(predicate.test(expense)); + + + } +} diff --git a/src/test/java/fasttrack/model/expense/ExpenseListTest.java b/src/test/java/fasttrack/model/expense/ExpenseListTest.java new file mode 100644 index 00000000000..efd735bf858 --- /dev/null +++ b/src/test/java/fasttrack/model/expense/ExpenseListTest.java @@ -0,0 +1,73 @@ +package fasttrack.model.expense; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.time.LocalDate; + +import org.junit.jupiter.api.Test; + +import fasttrack.model.category.MiscellaneousCategory; +import fasttrack.model.category.UserDefinedCategory; +public class ExpenseListTest { + private final ExpenseList expenseList = new ExpenseList(); + private final LocalDate date = LocalDate.now(); + private final LocalDate newDate = LocalDate.of(2020, 1, 1); + private final UserDefinedCategory category = new UserDefinedCategory("test", "description"); + private final MiscellaneousCategory miscellaneousCategory = new MiscellaneousCategory(); + + @Test + public void addExpense() { + Expense expense = new Expense("test", 1.0, date, category); + expenseList.add(expense); + assertEquals(expenseList.asUnmodifiableList().get(0), expense); + } + + @Test + public void removeExpense() { + Expense expense = new Expense("test", 1.0, date, category); + expenseList.add(expense); + expenseList.remove(expense); + assertEquals(expenseList.asUnmodifiableList().size(), 0); + } + + @Test + public void setExpenseList() { + Expense expense = new Expense("test", 1.0, date, category); + Expense newExpense = new Expense("test", 1.0, newDate, miscellaneousCategory); + expenseList.add(expense); + ExpenseList newExpenseList = new ExpenseList(); + newExpenseList.add(newExpense); + expenseList.setExpenseList(newExpenseList); + assertEquals(expenseList.asUnmodifiableList().get(0), newExpense); + } + + @Test + public void asUnmodifiableList() { + Expense expense = new Expense("test", 1.0, date, category); + expenseList.add(expense); + assertEquals(expenseList.asUnmodifiableList().get(0), expense); + } + + @Test + public void getExpenseListSize() { + Expense expense = new Expense("test", 1.0, date, category); + expenseList.add(expense); + assertEquals(expenseList.getSize(), 1); + } + + @Test + public void getExpenseListTotal() { + Expense expense = new Expense("test", 1.0, date, category); + expenseList.add(expense); + assertEquals(expenseList.getTotalAmount(), 1.0); + } + + @Test + public void getExpenseListTotalWithMultipleExpenses() { + Expense expense = new Expense("test", 1.0, date, category); + Expense expense2 = new Expense("test", 2.0, date, category); + expenseList.add(expense); + expenseList.add(expense2); + assertEquals(expenseList.getTotalAmount(), 3.0); + } +} diff --git a/src/test/java/fasttrack/model/expense/ExpenseTest.java b/src/test/java/fasttrack/model/expense/ExpenseTest.java new file mode 100644 index 00000000000..7e774fc1100 --- /dev/null +++ b/src/test/java/fasttrack/model/expense/ExpenseTest.java @@ -0,0 +1,74 @@ +package fasttrack.model.expense; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +import fasttrack.model.category.MiscellaneousCategory; +import fasttrack.model.category.UserDefinedCategory; + +public class ExpenseTest { + private final Price price = new Price("1.0"); + private final Expense expense = new Expense("test", price, null, new MiscellaneousCategory()); + + @Test + public void getName() { + assertEquals("test", expense.getName()); + } + + @Test + public void getAmount() { + assertEquals(1.0, expense.getAmount()); + } + + @Test + public void getDate() { + assertEquals(null, expense.getDate()); + } + + @Test + public void getCategory() { + assertEquals(new MiscellaneousCategory(), expense.getCategory()); + } + + @Test + public void toStringTest() { + assertEquals("Name: test, Amount: $1.0, Date: null, Category: Miscellaneous", expense.toString()); + } + + @Test + public void setCategory() { + expense.setCategory(new UserDefinedCategory("new", "bleh")); + assertEquals(new UserDefinedCategory("new", "bleh"), expense.getCategory()); + } + + @Test + public void setAmount() { + expense.setAmount("2.0"); + assertEquals(2.0, expense.getAmount()); + } + + @Test + public void setName() { + expense.setName("test2"); + assertEquals("test2", expense.getName()); + } + + @Test + public void setDate() { + expense.setDate(null); + assertEquals(null, expense.getDate()); + } + + @Test + public void equals() { + Expense expense2 = new Expense("test", 1.0, null, new MiscellaneousCategory()); + assertEquals(expense, expense2); + } + + @Test + public void hashCodeTest() { + Expense expense2 = new Expense("test", 1.0, null, new MiscellaneousCategory()); + assertEquals(expense.hashCode(), expense2.hashCode()); + } +} diff --git a/src/test/java/fasttrack/model/expense/RecurringExpenseManagerTest.java b/src/test/java/fasttrack/model/expense/RecurringExpenseManagerTest.java new file mode 100644 index 00000000000..c1a1fe6ce0d --- /dev/null +++ b/src/test/java/fasttrack/model/expense/RecurringExpenseManagerTest.java @@ -0,0 +1,40 @@ +package fasttrack.model.expense; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.time.LocalDate; +import java.util.ArrayList; + +import org.junit.jupiter.api.Test; + +import fasttrack.model.category.MiscellaneousCategory; + +public class RecurringExpenseManagerTest { + private LocalDate startDate = LocalDate.of(2023, 1, 1); + private LocalDate endDate = LocalDate.of(2023, 1, 1); + private MiscellaneousCategory miscellaneousCategory = new MiscellaneousCategory(); + + private final RecurringExpenseManager recurringExpenseManager = new RecurringExpenseManager("test", 1.0, + miscellaneousCategory, startDate, endDate, RecurringExpenseType.MONTHLY); + + private final ArrayList expenseList = recurringExpenseManager.getExpenses(); + + @Test + public void getExpenses() { + assertEquals(expenseList.get(0).getName(), "test"); + assertEquals(expenseList.get(0).getAmount(), 1.0); + assertEquals(expenseList.get(0).getCategory(), miscellaneousCategory); + assertEquals(expenseList.get(0).getDate(), startDate); + } + + @Test + public void getExpenseListSize() { + assertEquals(recurringExpenseManager.getNumberOfExpenses(), expenseList.size()); + } + + @Test + public void getExpenseListTotal() { + assertEquals(recurringExpenseManager.getTotalAmount(), + expenseList.get(0).getAmount() * expenseList.size()); + } +} diff --git a/src/test/java/fasttrack/model/util/CommandUtilityTest.java b/src/test/java/fasttrack/model/util/CommandUtilityTest.java new file mode 100644 index 00000000000..67c4eaff0d9 --- /dev/null +++ b/src/test/java/fasttrack/model/util/CommandUtilityTest.java @@ -0,0 +1,28 @@ +package fasttrack.model.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.time.LocalDate; + +import org.junit.jupiter.api.Test; + +class CommandUtilityTest { + + @Test + void testParseDateFromUserInput_validFormats_success() { + assertEquals(LocalDate.of(2023, 3, 1), CommandUtility.parseDateFromUserInput("1/3/23")); + assertEquals(LocalDate.of(2023, 6, 5), CommandUtility.parseDateFromUserInput("5/6/2023")); + assertEquals(LocalDate.of(2022, 7, 16), CommandUtility.parseDateFromUserInput("16/7/22")); + assertEquals(LocalDate.of(2021, 12, 31), CommandUtility.parseDateFromUserInput("31/12/2021")); + } + @Test + void testParseDateFromUserInput_invalidFormats_throwsException() { + assertThrows(IllegalArgumentException.class, () -> CommandUtility.parseDateFromUserInput("32/2/2022")); + assertThrows(IllegalArgumentException.class, () -> CommandUtility.parseDateFromUserInput("12/13/2023")); + assertThrows(IllegalArgumentException.class, () -> CommandUtility.parseDateFromUserInput("12w/12/23a")); + assertThrows(IllegalArgumentException.class, () -> CommandUtility.parseDateFromUserInput("12/c12/23a")); + assertThrows(IllegalArgumentException.class, () -> CommandUtility.parseDateFromUserInput("2023-4-31")); + } + +} diff --git a/src/test/java/fasttrack/model/util/StorageUtilityTest.java b/src/test/java/fasttrack/model/util/StorageUtilityTest.java new file mode 100644 index 00000000000..1c498fff157 --- /dev/null +++ b/src/test/java/fasttrack/model/util/StorageUtilityTest.java @@ -0,0 +1,26 @@ +package fasttrack.model.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.time.LocalDate; + +import org.junit.jupiter.api.Test; + + +public class StorageUtilityTest { + + @Test + public void testParseDateFromJson_validDate() { + LocalDate expectedDate = LocalDate.of(2023, 4, 30); + LocalDate parsedDate = StorageUtility.parseDateFromJson("2023-04-30"); + assertEquals(expectedDate, parsedDate); + } + + @Test + public void testParseDateFromJson_invalidDate() { + assertThrows(java.time.format.DateTimeParseException.class, () -> { + StorageUtility.parseDateFromJson("20-4-31"); + }); + } +} diff --git a/src/test/java/fasttrack/model/util/UserInterfaceUtilTest.java b/src/test/java/fasttrack/model/util/UserInterfaceUtilTest.java new file mode 100644 index 00000000000..97f48ee062c --- /dev/null +++ b/src/test/java/fasttrack/model/util/UserInterfaceUtilTest.java @@ -0,0 +1,52 @@ +package fasttrack.model.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.time.LocalDate; + +import org.junit.jupiter.api.Test; + +public class UserInterfaceUtilTest { + + @Test + public void testParseDate_validDate_success() { + LocalDate date1 = LocalDate.of(2023, 4, 10); + String formattedDate1 = UserInterfaceUtil.parseDate(date1); + assertEquals("10/04/23", formattedDate1); + LocalDate date2 = LocalDate.of(2023, 12, 31); + String formattedDate2 = UserInterfaceUtil.parseDate(date2); + assertEquals("31/12/23", formattedDate2); + } + + @Test + public void testParsePrice_validInput_success() { + double amount1 = 10.00; + String formattedAmount1 = UserInterfaceUtil.parsePrice(amount1); + assertEquals("$10.00", formattedAmount1); + + double amount2 = -5.50; + String formattedAmount2 = UserInterfaceUtil.parsePrice(amount2); + assertEquals("$-5.50", formattedAmount2); + + double amount3 = 3.14159; + String formattedAmount3 = UserInterfaceUtil.parsePrice(amount3); + assertEquals("$3.14", formattedAmount3); + } + + @Test + public void testCapitalizeFirstLetter_validInput_success() { + String input1 = "hello"; + String capitalized1 = UserInterfaceUtil.capitalizeFirstLetter(input1); + assertEquals("Hello", capitalized1); + + String input2 = "wORLD"; + String capitalized2 = UserInterfaceUtil.capitalizeFirstLetter(input2); + assertEquals("WORLD", capitalized2); + + String input3 = "TEST"; + String capitalized3 = UserInterfaceUtil.capitalizeFirstLetter(input3); + assertEquals("TEST", capitalized3); + } + +} + diff --git a/src/test/java/fasttrack/storage/JsonAdaptedCategoryTest.java b/src/test/java/fasttrack/storage/JsonAdaptedCategoryTest.java new file mode 100644 index 00000000000..6c572f5bfcf --- /dev/null +++ b/src/test/java/fasttrack/storage/JsonAdaptedCategoryTest.java @@ -0,0 +1,47 @@ +package fasttrack.storage; + +import static fasttrack.storage.JsonAdaptedCategory.MISSING_FIELD_MESSAGE_FORMAT; +import static fasttrack.testutil.Assert.assertThrows; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +import fasttrack.commons.exceptions.IllegalValueException; +import fasttrack.model.category.MiscellaneousCategory; +import fasttrack.model.category.UserDefinedCategory; + +public class JsonAdaptedCategoryTest { + private static final String VALID_CATEGORY = "Category"; + private static final String VALID_SUMMARY = "Description"; + @Test + public void toModelType_validCategoryDetails_returnsCategory() throws Exception { + JsonAdaptedCategory category = new JsonAdaptedCategory( + new UserDefinedCategory(VALID_CATEGORY, VALID_SUMMARY)); + assertEquals(VALID_CATEGORY, category.toModelType().getCategoryName()); + assertEquals(VALID_SUMMARY, category.toModelType().getSummary()); + } + + @Test + public void toModelType_nullCategory_throwsIllegalValueException() { + JsonAdaptedCategory category = new JsonAdaptedCategory( + new UserDefinedCategory(null, VALID_SUMMARY)); + String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, "Category"); + assertThrows(IllegalValueException.class, expectedMessage, category::toModelType); + } + + @Test + public void toModelType_nullSummary_throwsIllegalValueException() { + JsonAdaptedCategory category = new JsonAdaptedCategory( + new UserDefinedCategory(VALID_CATEGORY, null)); + String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, "Summary"); + assertThrows(IllegalValueException.class, expectedMessage, category::toModelType); + } + + @Test + public void toModelType_miscellaneousCategory_returnsCategory() throws Exception { + JsonAdaptedCategory category = new JsonAdaptedCategory( + new MiscellaneousCategory()); + assertEquals("Misc", category.toModelType().getCategoryName()); + assertEquals("Placeholder Description", category.toModelType().getSummary()); + } +} diff --git a/src/test/java/fasttrack/storage/JsonAdaptedExpenseTest.java b/src/test/java/fasttrack/storage/JsonAdaptedExpenseTest.java new file mode 100644 index 00000000000..d1a0dd8fe35 --- /dev/null +++ b/src/test/java/fasttrack/storage/JsonAdaptedExpenseTest.java @@ -0,0 +1,38 @@ +package fasttrack.storage; + +import static fasttrack.storage.JsonAdaptedExpense.MISSING_FIELD_MESSAGE_FORMAT; +import static fasttrack.testutil.Assert.assertThrows; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.time.LocalDate; + +import org.junit.jupiter.api.Test; + +import fasttrack.commons.exceptions.IllegalValueException; +import fasttrack.model.category.Category; +import fasttrack.model.category.MiscellaneousCategory; +import fasttrack.model.expense.Expense; + +public class JsonAdaptedExpenseTest { + private static final String VALID_NAME = "Benson"; + private static final double VALID_AMOUNT = 23; + private static final LocalDate VALID_DATE = LocalDate.now(); + private static final Category VALID_CATEGORY = new MiscellaneousCategory(); + + @Test + public void toModelType_validExpenseDetails_returnsExpense() throws Exception { + JsonAdaptedExpense expense = new JsonAdaptedExpense( + new Expense(VALID_NAME, VALID_AMOUNT, VALID_DATE, VALID_CATEGORY)); + assertEquals(VALID_NAME, expense.toModelType().getName()); + assertEquals(VALID_AMOUNT, expense.toModelType().getAmount()); + assertEquals(VALID_DATE, expense.toModelType().getDate()); + } + + @Test + public void toModelType_nullName_throwsIllegalValueException() { + JsonAdaptedExpense expense = new JsonAdaptedExpense( + new Expense(null, VALID_AMOUNT, VALID_DATE, VALID_CATEGORY)); + String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, "Name"); + assertThrows(IllegalValueException.class, expectedMessage, expense::toModelType); + } +} diff --git a/src/test/java/fasttrack/storage/JsonAdaptedRecurringExpenseManagerTest.java b/src/test/java/fasttrack/storage/JsonAdaptedRecurringExpenseManagerTest.java new file mode 100644 index 00000000000..ffc1e9b1fdb --- /dev/null +++ b/src/test/java/fasttrack/storage/JsonAdaptedRecurringExpenseManagerTest.java @@ -0,0 +1,45 @@ +package fasttrack.storage; + +import static fasttrack.testutil.Assert.assertThrows; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.time.LocalDate; + +import org.junit.jupiter.api.Test; + +import fasttrack.commons.exceptions.IllegalValueException; +import fasttrack.model.category.Category; +import fasttrack.model.category.UserDefinedCategory; +import fasttrack.model.expense.Price; +import fasttrack.model.expense.RecurringExpenseManager; +import fasttrack.model.expense.RecurringExpenseType; + +public class JsonAdaptedRecurringExpenseManagerTest { + + public static final String MISSING_FIELD_MESSAGE_FORMAT = "Recurring Expense Manager's field is missing!"; + private static final String VALID_EXPENSENAME = "sampleExpenseName"; + private static final double VALID_AMOUNT = 23; + private static final LocalDate VALID_STARTDATE = LocalDate.now(); + private static final Category VALID_CATEGORY = new UserDefinedCategory("sampleCat", "sampleSum"); + private static final RecurringExpenseType VALID_FREQUENCY = RecurringExpenseType.WEEKLY; + + @Test + public void toModelType_validRecurringExpenseManager() throws IllegalValueException { + JsonAdaptedRecurringExpenseManager recurringExpenseManager = new JsonAdaptedRecurringExpenseManager( + new RecurringExpenseManager(VALID_EXPENSENAME, new Price(VALID_AMOUNT), VALID_CATEGORY, + VALID_STARTDATE, VALID_FREQUENCY)); + assertEquals(VALID_EXPENSENAME, recurringExpenseManager.toModelType().getExpenseName()); + assertEquals(VALID_AMOUNT, recurringExpenseManager.toModelType().getAmount()); + assertEquals(VALID_STARTDATE, recurringExpenseManager.toModelType().getExpenseStartDate()); + assertEquals(VALID_FREQUENCY, recurringExpenseManager.toModelType().getRecurringExpenseType()); + } + + @Test + public void toModelType_nullName_throwsIllegalValueException() throws IllegalValueException { + JsonAdaptedRecurringExpenseManager recurringExpenseManager = new JsonAdaptedRecurringExpenseManager( + new RecurringExpenseManager(null, new Price(VALID_AMOUNT), VALID_CATEGORY, VALID_STARTDATE, + VALID_FREQUENCY)); + String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT); + assertThrows(IllegalValueException.class, expectedMessage, recurringExpenseManager::toModelType); + } +} diff --git a/src/test/java/fasttrack/storage/JsonExpenseTrackerStorageTest.java b/src/test/java/fasttrack/storage/JsonExpenseTrackerStorageTest.java new file mode 100644 index 00000000000..0849fa82323 --- /dev/null +++ b/src/test/java/fasttrack/storage/JsonExpenseTrackerStorageTest.java @@ -0,0 +1,82 @@ +package fasttrack.storage; + +import static fasttrack.testutil.Assert.assertThrows; +import static org.junit.jupiter.api.Assertions.assertFalse; + +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import fasttrack.commons.exceptions.DataConversionException; +import fasttrack.model.ExpenseTracker; +import fasttrack.model.ReadOnlyExpenseTracker; + +public class JsonExpenseTrackerStorageTest { + private static final Path TEST_DATA_FOLDER = Paths.get("src", "test", "data", "JsonAddressBookStorageTest"); + + @TempDir + public Path testFolder; + + @Test + public void readExpenseTracker_nullFilePath_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> readExpenseTracker(null)); + } + + private java.util.Optional readExpenseTracker(String filePath) throws Exception { + return new JsonExpenseTrackerStorage(Paths.get(filePath)) + .readExpenseTracker(addToTestDataPathIfNotNull(filePath)); + } + + private Path addToTestDataPathIfNotNull(String prefsFileInTestDataFolder) { + return prefsFileInTestDataFolder != null + ? TEST_DATA_FOLDER.resolve(prefsFileInTestDataFolder) + : null; + } + + @Test + public void read_missingFile_emptyResult() throws Exception { + assertFalse(readExpenseTracker("NonExistentFile.json").isPresent()); + } + + @Test + public void read_notJsonFormat_exceptionThrown() { + assertThrows(DataConversionException.class, () -> readExpenseTracker("notJsonFormatExpenseTracker.json")); + } + /** + * Valid Category data + * Invalid Category data + * Valid Description data + * Invalid Description data + * Valid Amount data + * Invalid Amount data + * Valid Date data + * Invalid Date data + * Valid Expense data + * Invalid Expense data + * Combinations + */ + @Test + public void saveExpenseTracker_nullExpenseTracker_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> saveExpenseTracker(null, "SomeFile.json")); + } + + /** + * Saves {@code expenseTracker} at the specified {@code filePath}. + */ + private void saveExpenseTracker(ReadOnlyExpenseTracker expenseTracker, String filePath) { + try { + new JsonExpenseTrackerStorage(Paths.get(filePath)) + .saveExpenseTracker(expenseTracker, addToTestDataPathIfNotNull(filePath)); + } catch (IOException ioe) { + throw new AssertionError("There should not be an error writing to the file.", ioe); + } + } + + @Test + public void saveExpenseTracker_nullFilePath_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> saveExpenseTracker(new ExpenseTracker(), null)); + } +} diff --git a/src/test/java/seedu/address/storage/JsonUserPrefsStorageTest.java b/src/test/java/fasttrack/storage/JsonUserPrefsStorageTest.java similarity index 93% rename from src/test/java/seedu/address/storage/JsonUserPrefsStorageTest.java rename to src/test/java/fasttrack/storage/JsonUserPrefsStorageTest.java index 16f33f4a6bb..303957871aa 100644 --- a/src/test/java/seedu/address/storage/JsonUserPrefsStorageTest.java +++ b/src/test/java/fasttrack/storage/JsonUserPrefsStorageTest.java @@ -1,8 +1,8 @@ -package seedu.address.storage; +package fasttrack.storage; +import static fasttrack.testutil.Assert.assertThrows; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; -import static seedu.address.testutil.Assert.assertThrows; import java.io.IOException; import java.nio.file.Path; @@ -12,10 +12,9 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; -import seedu.address.commons.core.GuiSettings; -import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.model.UserPrefs; - +import fasttrack.commons.core.GuiSettings; +import fasttrack.commons.exceptions.DataConversionException; +import fasttrack.model.UserPrefs; public class JsonUserPrefsStorageTest { private static final Path TEST_DATA_FOLDER = Paths.get("src", "test", "data", "JsonUserPrefsStorageTest"); @@ -73,7 +72,7 @@ public void readUserPrefs_extraValuesInFile_extraValuesIgnored() throws DataConv private UserPrefs getTypicalUserPrefs() { UserPrefs userPrefs = new UserPrefs(); userPrefs.setGuiSettings(new GuiSettings(1000, 500, 300, 100)); - userPrefs.setAddressBookFilePath(Paths.get("addressbook.json")); + userPrefs.setExpenseTrackerFilePath(Paths.get("data", "fastTrack.json")); return userPrefs; } diff --git a/src/test/java/seedu/address/storage/StorageManagerTest.java b/src/test/java/fasttrack/storage/StorageManagerTest.java similarity index 64% rename from src/test/java/seedu/address/storage/StorageManagerTest.java rename to src/test/java/fasttrack/storage/StorageManagerTest.java index 99a16548970..6dcb462edd1 100644 --- a/src/test/java/seedu/address/storage/StorageManagerTest.java +++ b/src/test/java/fasttrack/storage/StorageManagerTest.java @@ -1,8 +1,7 @@ -package seedu.address.storage; +package fasttrack.storage; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; import java.nio.file.Path; @@ -10,10 +9,8 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; -import seedu.address.commons.core.GuiSettings; -import seedu.address.model.AddressBook; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.UserPrefs; +import fasttrack.commons.core.GuiSettings; +import fasttrack.model.UserPrefs; public class StorageManagerTest { @@ -24,7 +21,7 @@ public class StorageManagerTest { @BeforeEach public void setUp() { - JsonAddressBookStorage addressBookStorage = new JsonAddressBookStorage(getTempFilePath("ab")); + JsonExpenseTrackerStorage addressBookStorage = new JsonExpenseTrackerStorage(getTempFilePath("ab")); JsonUserPrefsStorage userPrefsStorage = new JsonUserPrefsStorage(getTempFilePath("prefs")); storageManager = new StorageManager(addressBookStorage, userPrefsStorage); } @@ -36,9 +33,11 @@ private Path getTempFilePath(String fileName) { @Test public void prefsReadSave() throws Exception { /* - * Note: This is an integration test that verifies the StorageManager is properly wired to the + * Note: This is an integration test that verifies the StorageManager is + * properly wired to the * {@link JsonUserPrefsStorage} class. - * More extensive testing of UserPref saving/reading is done in {@link JsonUserPrefsStorageTest} class. + * More extensive testing of UserPref saving/reading is done in {@link + * JsonUserPrefsStorageTest} class. */ UserPrefs original = new UserPrefs(); original.setGuiSettings(new GuiSettings(300, 600, 4, 6)); @@ -50,19 +49,21 @@ public void prefsReadSave() throws Exception { @Test public void addressBookReadSave() throws Exception { /* - * Note: This is an integration test that verifies the StorageManager is properly wired to the + * Note: This is an integration test that verifies the StorageManager is + * properly wired to the * {@link JsonAddressBookStorage} class. - * More extensive testing of UserPref saving/reading is done in {@link JsonAddressBookStorageTest} class. + * More extensive testing of UserPref saving/reading is done in {@link + * JsonAddressBookStorageTest} class. */ - AddressBook original = getTypicalAddressBook(); - storageManager.saveAddressBook(original); - ReadOnlyAddressBook retrieved = storageManager.readAddressBook().get(); - assertEquals(original, new AddressBook(retrieved)); + //ExpenseTracker original = getTypicalExpenseTracker(); + //storageManager.saveExpenseTracker(original); + //ReadOnlyExpenseTracker retrieved = storageManager.readExpenseTracker().get(); + //assertEquals(original, new ExpenseTracker(retrieved)); } @Test public void getAddressBookFilePath() { - assertNotNull(storageManager.getAddressBookFilePath()); + assertNotNull(storageManager.getExpenseTrackerFilePath()); } } diff --git a/src/test/java/seedu/address/testutil/Assert.java b/src/test/java/fasttrack/testutil/Assert.java similarity index 97% rename from src/test/java/seedu/address/testutil/Assert.java rename to src/test/java/fasttrack/testutil/Assert.java index 9863093bd6e..fa383704c74 100644 --- a/src/test/java/seedu/address/testutil/Assert.java +++ b/src/test/java/fasttrack/testutil/Assert.java @@ -1,4 +1,4 @@ -package seedu.address.testutil; +package fasttrack.testutil; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.function.Executable; diff --git a/src/test/java/seedu/address/testutil/SerializableTestClass.java b/src/test/java/fasttrack/testutil/SerializableTestClass.java similarity index 98% rename from src/test/java/seedu/address/testutil/SerializableTestClass.java rename to src/test/java/fasttrack/testutil/SerializableTestClass.java index f5a66340489..8ca77a71dbc 100644 --- a/src/test/java/seedu/address/testutil/SerializableTestClass.java +++ b/src/test/java/fasttrack/testutil/SerializableTestClass.java @@ -1,4 +1,4 @@ -package seedu.address.testutil; +package fasttrack.testutil; import java.time.LocalDateTime; import java.util.ArrayList; diff --git a/src/test/java/seedu/address/testutil/TestUtil.java b/src/test/java/fasttrack/testutil/TestUtil.java similarity index 52% rename from src/test/java/seedu/address/testutil/TestUtil.java rename to src/test/java/fasttrack/testutil/TestUtil.java index 896d103eb0b..150488d0662 100644 --- a/src/test/java/seedu/address/testutil/TestUtil.java +++ b/src/test/java/fasttrack/testutil/TestUtil.java @@ -1,13 +1,13 @@ -package seedu.address.testutil; +package fasttrack.testutil; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import seedu.address.commons.core.index.Index; -import seedu.address.model.Model; -import seedu.address.model.person.Person; +import fasttrack.commons.core.index.Index; +import fasttrack.model.Model; +import fasttrack.model.expense.Expense; /** * A utility class for test cases. @@ -33,23 +33,23 @@ public static Path getFilePathInSandboxFolder(String fileName) { } /** - * Returns the middle index of the person in the {@code model}'s person list. + * Returns the middle index of the expense in the {@code model}'s expense list. */ - public static Index getMidIndex(Model model) { - return Index.fromOneBased(model.getFilteredPersonList().size() / 2); + public static Index getMidIndex(Model dataModel) { + return Index.fromOneBased(dataModel.getFilteredExpenseList().size() / 2); } /** - * Returns the last index of the person in the {@code model}'s person list. + * Returns the last index of the expense in the {@code model}'s expense list. */ - public static Index getLastIndex(Model model) { - return Index.fromOneBased(model.getFilteredPersonList().size()); + public static Index getLastIndex(Model dataModel) { + return Index.fromOneBased(dataModel.getFilteredExpenseList().size()); } /** - * Returns the person in the {@code model}'s person list at {@code index}. + * Returns the expense in the {@code model}'s expense list at {@code index}. */ - public static Person getPerson(Model model, Index index) { - return model.getFilteredPersonList().get(index.getZeroBased()); + public static Expense getPerson(Model dataModel, Index index) { + return dataModel.getFilteredExpenseList().get(index.getZeroBased()); } } diff --git a/src/test/java/fasttrack/testutil/TypicalCategories.java b/src/test/java/fasttrack/testutil/TypicalCategories.java new file mode 100644 index 00000000000..efdd4087d3d --- /dev/null +++ b/src/test/java/fasttrack/testutil/TypicalCategories.java @@ -0,0 +1,50 @@ +package fasttrack.testutil; + + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import fasttrack.model.Budget; +import fasttrack.model.ExpenseTracker; +import fasttrack.model.category.Category; +import fasttrack.model.category.MiscellaneousCategory; +import fasttrack.model.category.UserDefinedCategory; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +/** + * A utility class containing a list of {@code Category} objects to be used in tests. + */ +public class TypicalCategories { + + public static final Category MISCCAT = new MiscellaneousCategory(); + public static final Category FOOD_NO_SUMMARY = new UserDefinedCategory("food", ""); + public static final Category FOOD = new UserDefinedCategory("food", "For consumable expenses"); + public static final Category TECH = new UserDefinedCategory("tech", "For electronics"); + public static final Category SCHOOL = new UserDefinedCategory("school", "School expenses"); + + public static final ObservableList TYPICAL_CATEGORIES = FXCollections.observableArrayList( + MISCCAT, FOOD, TECH, SCHOOL + ); + public static final Category FITNESS = new UserDefinedCategory("fitness", "for fitness related expenses"); + public static final Category ENTERTAINMENT = new UserDefinedCategory("entertainment", + "for entertainment expenses"); + public static final Category HOUSING = new UserDefinedCategory("housing", "housing payments"); + public static final Category UTILITIES = new UserDefinedCategory("utilities", "utility bills"); + /** + * Returns an {@code ExpenseTracker} with all the typical categories. + */ + public static ExpenseTracker getTypicalExpenseTracker() { + ExpenseTracker et = new ExpenseTracker(); + for (Category category : getTypicalCategory()) { + et.addCategory(category); + } + et.setBudget(new Budget(1000)); + return et; + } + + public static List getTypicalCategory() { + return new ArrayList<>(Arrays.asList(FOOD_NO_SUMMARY, TECH, SCHOOL, FITNESS, ENTERTAINMENT, HOUSING)); + } +} diff --git a/src/test/java/fasttrack/testutil/TypicalExpenses.java b/src/test/java/fasttrack/testutil/TypicalExpenses.java new file mode 100644 index 00000000000..2cf97ac2dee --- /dev/null +++ b/src/test/java/fasttrack/testutil/TypicalExpenses.java @@ -0,0 +1,63 @@ +package fasttrack.testutil; + +import static fasttrack.testutil.TypicalCategories.FOOD; +import static fasttrack.testutil.TypicalCategories.MISCCAT; +import static fasttrack.testutil.TypicalCategories.SCHOOL; +import static fasttrack.testutil.TypicalCategories.TECH; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import fasttrack.model.Budget; +import fasttrack.model.ExpenseTracker; +import fasttrack.model.expense.Expense; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + + +/** + * A utility class containing a list of {@code Expense} objects to be used in tests. + */ +public class TypicalExpenses { + + // For FastTrack + public static final Expense APPLE = + new Expense("Apple", 1.50, LocalDate.of(2023, 3, 1), FOOD); + public static final Expense BANANA = + new Expense("Banana", 1.00, LocalDate.of(2023, 3, 2), FOOD); + public static final Expense CHERRY = + new Expense("Cherry", 0.20, LocalDate.of(2023, 3, 1), TECH); + public static final Expense DURIAN = + new Expense("Durian", 15, LocalDate.of(2023, 3, 15), TECH); + public static final Expense ELDERBERRY = + new Expense("Elderberry", 4, LocalDate.of(2022, 1, 1), SCHOOL); + public static final Expense FIG = + new Expense("Fig", 1000, LocalDate.of(2023, 2, 15), MISCCAT); + public static final Expense GRAPE = + new Expense("Grape", 10, LocalDate.of(2023, 3, 17), MISCCAT); + + public static final ObservableList TYPICAL_EXPENSES = FXCollections.observableArrayList( + APPLE, BANANA, CHERRY, DURIAN, ELDERBERRY, FIG, GRAPE + ); + + + private TypicalExpenses() {} // prevents instantiation + + /** + * Returns an {@code ExpenseTracker} with all the typical expenses. + */ + public static ExpenseTracker getTypicalExpenseTracker() { + ExpenseTracker et = new ExpenseTracker(); + for (Expense expense : getTypicalExpenses()) { + et.addExpense(expense); + } + et.setBudget(new Budget(1000)); + return et; + } + + public static List getTypicalExpenses() { + return new ArrayList<>(Arrays.asList(APPLE, BANANA, CHERRY, DURIAN, ELDERBERRY, FIG, GRAPE)); + } +} diff --git a/src/test/java/seedu/address/testutil/TypicalIndexes.java b/src/test/java/fasttrack/testutil/TypicalIndexes.java similarity index 81% rename from src/test/java/seedu/address/testutil/TypicalIndexes.java rename to src/test/java/fasttrack/testutil/TypicalIndexes.java index 1e613937657..8e78e11f88a 100644 --- a/src/test/java/seedu/address/testutil/TypicalIndexes.java +++ b/src/test/java/fasttrack/testutil/TypicalIndexes.java @@ -1,6 +1,6 @@ -package seedu.address.testutil; +package fasttrack.testutil; -import seedu.address.commons.core.index.Index; +import fasttrack.commons.core.index.Index; /** * A utility class containing a list of {@code Index} objects to be used in tests. diff --git a/src/test/java/fasttrack/testutil/TypicalRecurringExpenseManagers.java b/src/test/java/fasttrack/testutil/TypicalRecurringExpenseManagers.java new file mode 100644 index 00000000000..5700937fecd --- /dev/null +++ b/src/test/java/fasttrack/testutil/TypicalRecurringExpenseManagers.java @@ -0,0 +1,64 @@ +package fasttrack.testutil; + +import static fasttrack.testutil.TypicalCategories.FOOD; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import fasttrack.model.Budget; +import fasttrack.model.ExpenseTracker; +import fasttrack.model.expense.RecurringExpenseManager; +import fasttrack.model.expense.RecurringExpenseType; + +/** + * A utility class containing a list of {@code RecurringExpenseManager} objects to be used in tests. + */ +public class TypicalRecurringExpenseManagers { + + public static final RecurringExpenseManager RECUR_APPLE_DAY = + new RecurringExpenseManager("Apple", 1.50, FOOD, + LocalDate.of(2023, 3, 1), RecurringExpenseType.DAILY); + public static final RecurringExpenseManager RECUR_APPLE_DAY_WITH_END = + new RecurringExpenseManager("Apple", 1.50, FOOD, + LocalDate.of(2023, 3, 1), LocalDate.of(2024, 3, 2), + RecurringExpenseType.DAILY); + public static final RecurringExpenseManager RECUR_APPLE_WEEK = + new RecurringExpenseManager("Apple", 1.50, FOOD, + LocalDate.of(2023, 3, 1), RecurringExpenseType.WEEKLY); + public static final RecurringExpenseManager RECUR_APPLE_WEEK_WITH_END = + new RecurringExpenseManager("Apple", 1.50, FOOD, + LocalDate.of(2023, 3, 1), LocalDate.of(2024, 3, 2), + RecurringExpenseType.WEEKLY); + public static final RecurringExpenseManager RECUR_APPLE_MONTH = + new RecurringExpenseManager("Apple", 1.50, FOOD, + LocalDate.of(2023, 3, 1), RecurringExpenseType.MONTHLY); + public static final RecurringExpenseManager RECUR_APPLE_MONTH_WITH_END = + new RecurringExpenseManager("Apple", 1.50, FOOD, + LocalDate.of(2023, 3, 1), LocalDate.of(2024, 3, 2), + RecurringExpenseType.MONTHLY); + public static final RecurringExpenseManager RECUR_APPLE_YEAR = + new RecurringExpenseManager("Apple", 1.50, FOOD, + LocalDate.of(2023, 3, 1), RecurringExpenseType.YEARLY); + public static final RecurringExpenseManager RECUR_APPLE_YEAR_WITH_END = + new RecurringExpenseManager("Apple", 1.50, FOOD, + LocalDate.of(2023, 3, 1), LocalDate.of(2024, 3, 2), + RecurringExpenseType.YEARLY); + + /** + * Returns an {@code ExpenseTracker} with all the typical expenses. + */ + public static ExpenseTracker getTypicalExpenseTracker() { + ExpenseTracker et = new ExpenseTracker(); + for (RecurringExpenseManager manager : getTypicalManagers()) { + et.addRecurringGenerator(manager); + } + et.setBudget(new Budget(1000)); + return et; + } + + public static List getTypicalManagers() { + return new ArrayList<>(Arrays.asList(RECUR_APPLE_YEAR)); + } +} diff --git a/src/test/java/fasttrack/testutil/TypicalRecurringExpenses.java b/src/test/java/fasttrack/testutil/TypicalRecurringExpenses.java new file mode 100644 index 00000000000..9268d5da32c --- /dev/null +++ b/src/test/java/fasttrack/testutil/TypicalRecurringExpenses.java @@ -0,0 +1,36 @@ +package fasttrack.testutil; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import fasttrack.model.expense.RecurringExpenseManager; +import fasttrack.model.expense.RecurringExpenseType; + + +/** + * A utility class containing a list of {@code RecurringExpenseManager} objects to be used in tests. + */ +public class TypicalRecurringExpenses { + + public static final RecurringExpenseManager GYM_MEMBERSHIP = + new RecurringExpenseManager("Gym Membership", 50, TypicalCategories.FITNESS, + LocalDate.of(2023, 4, 1), null, RecurringExpenseType.MONTHLY); + public static final RecurringExpenseManager NETFLIX_SUBSCRIPTION = + new RecurringExpenseManager("Netflix Subscription", 15, TypicalCategories.ENTERTAINMENT, + LocalDate.of(2023, 2, 20), null, RecurringExpenseType.MONTHLY); + public static final RecurringExpenseManager RENT = + new RecurringExpenseManager("Rent", 800, TypicalCategories.HOUSING, + LocalDate.of(2023, 1, 15), null, RecurringExpenseType.MONTHLY); + public static final RecurringExpenseManager INTERNET = + new RecurringExpenseManager("Water", 60, TypicalCategories.UTILITIES, + LocalDate.of(2023, 4, 1), LocalDate.of(2026, 4, 1), RecurringExpenseType.MONTHLY); + + private TypicalRecurringExpenses() {} // prevents instantiation + + public static List getTypicalRecurringExpenses() { + return new ArrayList<>(Arrays.asList(GYM_MEMBERSHIP, NETFLIX_SUBSCRIPTION, RENT, INTERNET)); + } +} + diff --git a/src/test/java/fasttrack/ui/CategoryCardTest.java b/src/test/java/fasttrack/ui/CategoryCardTest.java new file mode 100644 index 00000000000..d550c08cbfa --- /dev/null +++ b/src/test/java/fasttrack/ui/CategoryCardTest.java @@ -0,0 +1,105 @@ +package fasttrack.ui; + +import static fasttrack.testutil.TypicalCategories.FOOD; +import static fasttrack.ui.JavaFxTestHelper.initJavaFxHelper; +import static fasttrack.ui.JavaFxTestHelper.setUpHeadlessMode; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentest4j.AssertionFailedError; + +import fasttrack.model.category.Category; +import javafx.application.Platform; +import javafx.scene.control.Label; + + +public class CategoryCardTest { + + private Category category; + private int displayedIndex; + private int associatedExpenseCount; + + @BeforeEach + public void setUp() throws InterruptedException { + if (System.getProperty("os.name").toLowerCase().contains("linux")) { + return; + } + category = FOOD; + displayedIndex = 1; + associatedExpenseCount = 3; + } + + @BeforeAll + static void initJfx() throws InterruptedException { + if (System.getProperty("os.name").toLowerCase().contains("linux")) { + return; + } + setUpHeadlessMode(); + initJavaFxHelper(); + } + + @Test + public void testCategoryCard_validData_success() { + if (System.getProperty("os.name").toLowerCase().contains("linux")) { + return; + } + CategoryCard categoryCard = new CategoryCard(category, displayedIndex, associatedExpenseCount); + CompletableFuture future = new CompletableFuture<>(); + Platform.runLater(() -> { + try { + // Test that the category name label is set correctly + Label categoryNameLabel = (Label) categoryCard.getRoot().lookup("#categoryName"); + assertEquals("Food", categoryNameLabel.getText()); + + // Test that the index label is set correctly + Label indexLabel = (Label) categoryCard.getRoot().lookup("#id"); + assertEquals("1. ", indexLabel.getText()); + + // Test that the expense count label is set correctly + Label expenseCountLabel = (Label) categoryCard.getRoot().lookup("#expenseCount"); + assertEquals("3", expenseCountLabel.getText()); + future.complete(null); + } catch (AssertionFailedError e) { + future.completeExceptionally(e); + } + }); + try { + future.get(); + } catch (InterruptedException | ExecutionException e) { + fail("Assertion error thrown in Platform.runLater thread: " + e.getMessage()); + } + } + + @Test + public void testEquals_validCategoryCard_success() { + if (System.getProperty("os.name").toLowerCase().contains("linux")) { + return; + } + CategoryCard categoryCard1 = new CategoryCard(category, displayedIndex, associatedExpenseCount); + CategoryCard categoryCard2 = new CategoryCard(category, displayedIndex + 1, associatedExpenseCount - 1); + CategoryCard categoryCard3 = new CategoryCard(category, displayedIndex + 1, associatedExpenseCount - 1); + CompletableFuture future = new CompletableFuture<>(); + Platform.runLater(() -> { + try { + assertEquals(categoryCard1, categoryCard1); + assertNotEquals(categoryCard1, categoryCard2); + assertEquals(categoryCard2, categoryCard3); + future.complete(null); + } catch (AssertionFailedError e) { + future.completeExceptionally(e); + } + }); + try { + future.get(); + } catch (InterruptedException | ExecutionException e) { + fail("Assertion error thrown in Platform.runLater thread: " + e.getMessage()); + } + } +} diff --git a/src/test/java/fasttrack/ui/CategoryListPanelTest.java b/src/test/java/fasttrack/ui/CategoryListPanelTest.java new file mode 100644 index 00000000000..a158b96bf87 --- /dev/null +++ b/src/test/java/fasttrack/ui/CategoryListPanelTest.java @@ -0,0 +1,101 @@ +package fasttrack.ui; + +import static fasttrack.testutil.TypicalCategories.FOOD; +import static fasttrack.testutil.TypicalCategories.TECH; +import static fasttrack.testutil.TypicalExpenses.APPLE; +import static fasttrack.testutil.TypicalExpenses.CHERRY; +import static fasttrack.ui.JavaFxTestHelper.initJavaFxHelper; +import static fasttrack.ui.JavaFxTestHelper.setUpHeadlessMode; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentest4j.AssertionFailedError; + +import fasttrack.model.category.Category; +import fasttrack.model.expense.Expense; +import javafx.application.Platform; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.scene.control.ListView; + + +public class CategoryListPanelTest { + + private CategoryListPanel categoryListPanel; + private ObservableList categories; + private ObservableList expenses; + + @BeforeEach + public void setUp() { + if (System.getProperty("os.name").toLowerCase().contains("linux")) { + return; + } + categories = FXCollections.observableArrayList(FOOD, TECH); + expenses = FXCollections.observableArrayList(APPLE, CHERRY); + } + + @BeforeAll + static void initJfx() throws InterruptedException { + if (System.getProperty("os.name").toLowerCase().contains("linux")) { + return; + } + setUpHeadlessMode(); + initJavaFxHelper(); + } + + + @Test + public void categoryListView_validCategories_countEqual() { + if (System.getProperty("os.name").toLowerCase().contains("linux")) { + return; + } + CompletableFuture future = new CompletableFuture<>(); + categoryListPanel = new CategoryListPanel(categories, expenses); + Platform.runLater(() -> { + try { + // Test that the number of categories is correct + ListView categoryListView = (ListView) categoryListPanel.getRoot().lookup("#categoryListView"); + assertEquals(categories.size(), categoryListView.getItems().size()); + future.complete(null); + } catch (AssertionFailedError e) { + future.completeExceptionally(e); + } + }); + try { + future.get(); + } catch (InterruptedException | ExecutionException e) { + fail("Assertion error thrown in Platform.runLater thread: " + e.getMessage()); + } + } + + @Test + public void categoryListView_emptyList_countZero() { + if (System.getProperty("os.name").toLowerCase().contains("linux")) { + return; + } + categories = FXCollections.observableArrayList(); + expenses = FXCollections.observableArrayList(); + CompletableFuture future = new CompletableFuture<>(); + categoryListPanel = new CategoryListPanel(categories, expenses); + Platform.runLater(() -> { + try { + ListView categoryListView = (ListView) categoryListPanel.getRoot().lookup("#categoryListView"); + assertEquals(0, categoryListView.getItems().size()); + future.complete(null); + } catch (AssertionFailedError e) { + future.completeExceptionally(e); + } + }); + try { + future.get(); + } catch (InterruptedException | ExecutionException e) { + fail("Assertion error thrown in Platform.runLater thread: " + e.getMessage()); + } + } +} diff --git a/src/test/java/fasttrack/ui/CommandBoxTest.java b/src/test/java/fasttrack/ui/CommandBoxTest.java new file mode 100644 index 00000000000..d0c46050828 --- /dev/null +++ b/src/test/java/fasttrack/ui/CommandBoxTest.java @@ -0,0 +1,119 @@ +package fasttrack.ui; + +import static fasttrack.ui.JavaFxTestHelper.initJavaFxHelper; +import static fasttrack.ui.JavaFxTestHelper.setUpHeadlessMode; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Objects; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import fasttrack.logic.commands.exceptions.CommandException; +import javafx.collections.ObservableList; +import javafx.scene.Scene; +import javafx.scene.control.TextField; +import javafx.scene.layout.HBox; + + +public class CommandBoxTest { + + private static final String VALID_COMMAND = "add n/Milk c/Groceries p/12"; + private static final String INCOMPLETE_COMMAND = "add n/Milk c/Gro"; + private static final String ERROR_STYLE_CLASS = "error"; + + private CommandBox commandBox; + private boolean commandExecuted; + + + @BeforeEach + public void setUp() { + if (System.getProperty("os.name").toLowerCase().contains("linux")) { + return; + } + commandExecuted = false; + // Dummy command executor function + CommandBox.CommandExecutor commandExecutor = commandText -> { + if (Objects.equals(commandText, "invalid command")) { + throw new CommandException("Invalid command"); + } + commandExecuted = true; + return null; + }; + commandBox = new CommandBox(commandExecutor, false); + } + + @BeforeAll + static void initJfx() throws InterruptedException { + if (System.getProperty("os.name").toLowerCase().contains("linux")) { + return; + } + setUpHeadlessMode(); + initJavaFxHelper(); + } + + + @Test + public void handleCommandEntered_emptyCommand_commandNotExecuted() { + if (System.getProperty("os.name").toLowerCase().contains("linux")) { + return; + } + TextField textField = (TextField) commandBox.getRoot().lookup("#commandTextField"); + textField.setText(""); + commandBox.handleCommandEntered(); + assertFalse(commandExecuted); + } + + @Test + public void handleCommandEntered_validCommand_commandExecutedSuccessfully() { + if (System.getProperty("os.name").toLowerCase().contains("linux")) { + return; + } + TextField textField = (TextField) commandBox.getRoot().lookup("#commandTextField"); + textField.setText(VALID_COMMAND); + commandBox.handleCommandEntered(); + assertTrue(commandExecuted); + } + + @Test + public void handleCommandEntered_invalidCommand_setsStyleToIndicateCommandFailure() { + if (System.getProperty("os.name").toLowerCase().contains("linux")) { + return; + } + commandBox.getTextProperty().setValue("invalid command"); + commandBox.handleCommandEntered(); + TextField commandTextField = (TextField) commandBox.getRoot().lookup("#commandTextField"); + ObservableList styleClass = commandTextField.getStyleClass(); + assertTrue(styleClass.contains(ERROR_STYLE_CLASS)); + } + + @Test + public void setFocus_commandBox_getsFocus() { + if (System.getProperty("os.name").toLowerCase().contains("linux")) { + return; + } + TextField textField = (TextField) commandBox.getRoot().lookup("#commandTextField"); + HBox hbox = new HBox(); + hbox.getChildren().add(commandBox.getRoot()); + Scene scene = new Scene(hbox); + commandBox.setFocus(); + TextField focusedTextField = (TextField) scene.getFocusOwner(); + assertEquals(focusedTextField, textField); + } + + @Test + public void updateCommandBoxText_validCategoryName_updatesText() { + if (System.getProperty("os.name").toLowerCase().contains("linux")) { + return; + } + TextField textField = (TextField) commandBox.getRoot().lookup("#commandTextField"); + textField.setText(INCOMPLETE_COMMAND); + commandBox.updateCommandBoxText("Groceries"); + String expected = "add n/Milk c/Groceries "; + assertEquals(expected, textField.getText()); + } + +} diff --git a/src/test/java/fasttrack/ui/ExpenseCardTest.java b/src/test/java/fasttrack/ui/ExpenseCardTest.java new file mode 100644 index 00000000000..8de5f8e7d08 --- /dev/null +++ b/src/test/java/fasttrack/ui/ExpenseCardTest.java @@ -0,0 +1,111 @@ +package fasttrack.ui; + +import static fasttrack.testutil.TypicalExpenses.APPLE; +import static fasttrack.ui.JavaFxTestHelper.initJavaFxHelper; +import static fasttrack.ui.JavaFxTestHelper.setUpHeadlessMode; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentest4j.AssertionFailedError; + +import fasttrack.model.expense.Expense; +import javafx.application.Platform; +import javafx.scene.control.Label; + +class ExpenseCardTest { + + private Expense expense; + private int displayedIndex; + + @BeforeEach + public void setUp() { + if (System.getProperty("os.name").toLowerCase().contains("linux")) { + return; + } + expense = APPLE; + displayedIndex = 1; + } + + @BeforeAll + static void initJfx() throws InterruptedException { + if (System.getProperty("os.name").toLowerCase().contains("linux")) { + return; + } + setUpHeadlessMode(); + initJavaFxHelper(); + } + + @Test + public void testExpenseCard_validData_success() { + if (System.getProperty("os.name").toLowerCase().contains("linux")) { + return; + } + ExpenseCard expenseCard = new ExpenseCard(expense, displayedIndex); + CompletableFuture future = new CompletableFuture<>(); + Platform.runLater(() -> { + try { + // Test that the category name label is set correctly + Label expenseNameLabel = (Label) expenseCard.getRoot().lookup("#expenseName"); + assertEquals("Apple", expenseNameLabel.getText()); + + // Test that the index label is set correctly + Label indexLabel = (Label) expenseCard.getRoot().lookup("#id"); + assertEquals("1. ", indexLabel.getText()); + + // Test that the price label is set correctly + Label priceLabel = (Label) expenseCard.getRoot().lookup("#price"); + assertEquals("$1.50", priceLabel.getText()); + + // Test that the category label is set correctly + Label categoryLabel = (Label) expenseCard.getRoot().lookup("#category"); + assertEquals("Food", categoryLabel.getText()); + + // Test that the date label is set correctly + Label frequencyLabel = (Label) expenseCard.getRoot().lookup("#date"); + assertEquals("01/03/23", frequencyLabel.getText()); + future.complete(null); + } catch (AssertionFailedError e) { + future.completeExceptionally(e); + } + }); + try { + future.get(); + } catch (InterruptedException | ExecutionException e) { + fail("Assertion error thrown in Platform.runLater thread: " + e.getMessage()); + } + } + + @Test + public void testEquals_validExpenseCard_success() { + if (System.getProperty("os.name").toLowerCase().contains("linux")) { + return; + } + ExpenseCard expenseCard1 = new ExpenseCard(expense, displayedIndex); + ExpenseCard expenseCard2 = new ExpenseCard(expense, displayedIndex + 1); + ExpenseCard expenseCard3 = new ExpenseCard(expense, displayedIndex + 1); + CompletableFuture future = new CompletableFuture<>(); + Platform.runLater(() -> { + try { + assertEquals(expenseCard1, expenseCard1); + assertNotEquals(expenseCard1, expenseCard2); + assertEquals(expenseCard2, expenseCard3); + future.complete(null); + } catch (AssertionFailedError e) { + future.completeExceptionally(e); + } + }); + try { + future.get(); + } catch (InterruptedException | ExecutionException e) { + fail("Assertion error thrown in Platform.runLater thread: " + e.getMessage()); + } + } + +} diff --git a/src/test/java/fasttrack/ui/ExpenseListPanelTest.java b/src/test/java/fasttrack/ui/ExpenseListPanelTest.java new file mode 100644 index 00000000000..68ce06e28f0 --- /dev/null +++ b/src/test/java/fasttrack/ui/ExpenseListPanelTest.java @@ -0,0 +1,96 @@ +package fasttrack.ui; + +import static fasttrack.testutil.TypicalExpenses.APPLE; +import static fasttrack.testutil.TypicalExpenses.BANANA; +import static fasttrack.testutil.TypicalExpenses.CHERRY; +import static fasttrack.ui.JavaFxTestHelper.initJavaFxHelper; +import static fasttrack.ui.JavaFxTestHelper.setUpHeadlessMode; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentest4j.AssertionFailedError; + +import fasttrack.model.expense.Expense; +import javafx.application.Platform; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.scene.control.ListView; + + +class ExpenseListPanelTest { + + private ExpenseListPanel expensePanel; + private ObservableList expenses; + + @BeforeEach + public void setUp() { + if (System.getProperty("os.name").toLowerCase().contains("linux")) { + return; + } + expenses = FXCollections.observableArrayList(APPLE, BANANA, CHERRY); + } + + @BeforeAll + static void initJfx() throws InterruptedException { + if (System.getProperty("os.name").toLowerCase().contains("linux")) { + return; + } + setUpHeadlessMode(); + initJavaFxHelper(); + } + + + @Test + public void expenseListView_validExpenses_countEqual() { + if (System.getProperty("os.name").toLowerCase().contains("linux")) { + return; + } + expensePanel = new ExpenseListPanel(expenses); + CompletableFuture future = new CompletableFuture<>(); + Platform.runLater(() -> { + try { + // Test that the number of recurring expenses is correct + ListView expenseListView = (ListView) expensePanel.getRoot().lookup("#expenseListView"); + assertEquals(expenses.size(), expenseListView.getItems().size()); + future.complete(null); + } catch (AssertionFailedError e) { + future.completeExceptionally(e); + } + }); + try { + future.get(); + } catch (InterruptedException | ExecutionException e) { + fail("Assertion error thrown in Platform.runLater thread: " + e.getMessage()); + } + } + + @Test + public void expenseListView_validExpenses_countZero() { + if (System.getProperty("os.name").toLowerCase().contains("linux")) { + return; + } + expenses = FXCollections.observableArrayList(); + expensePanel = new ExpenseListPanel(expenses); + CompletableFuture future = new CompletableFuture<>(); + Platform.runLater(() -> { + try { + ListView expenseListView = (ListView) expensePanel.getRoot().lookup("#expenseListView"); + assertEquals(0, expenseListView.getItems().size()); + future.complete(null); + } catch (AssertionFailedError e) { + future.completeExceptionally(e); + } + }); + try { + future.get(); + } catch (InterruptedException | ExecutionException e) { + fail("Assertion error thrown in Platform.runLater thread: " + e.getMessage()); + } + } +} diff --git a/src/test/java/fasttrack/ui/JavaFxTestHelper.java b/src/test/java/fasttrack/ui/JavaFxTestHelper.java new file mode 100644 index 00000000000..86c1f1d6519 --- /dev/null +++ b/src/test/java/fasttrack/ui/JavaFxTestHelper.java @@ -0,0 +1,35 @@ +package fasttrack.ui; + +import java.util.concurrent.CountDownLatch; + +import javafx.application.Platform; + +/** + * Helper class for JavaFX UI test + */ +public class JavaFxTestHelper { + private static boolean isInitialized = false; + + /** + * Initialises JavaFX platform for UI test + */ + public static void initJavaFxHelper() throws InterruptedException { + if (System.getProperty("os.name").toLowerCase().contains("linux")) { + return; + } + if (!isInitialized) { + CountDownLatch latch = new CountDownLatch(1); + Platform.startup(latch::countDown); + latch.await(); + isInitialized = true; + } + } + + public static void setUpHeadlessMode() { + if (System.getProperty("os.name").toLowerCase().contains("linux")) { + return; + } + System.setProperty("java.awt.headless", "true"); + } + +} diff --git a/src/test/java/fasttrack/ui/RecurringExpenseCardTest.java b/src/test/java/fasttrack/ui/RecurringExpenseCardTest.java new file mode 100644 index 00000000000..ed222d500c1 --- /dev/null +++ b/src/test/java/fasttrack/ui/RecurringExpenseCardTest.java @@ -0,0 +1,114 @@ +package fasttrack.ui; + +import static fasttrack.testutil.TypicalRecurringExpenses.GYM_MEMBERSHIP; +import static fasttrack.ui.JavaFxTestHelper.initJavaFxHelper; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentest4j.AssertionFailedError; + +import fasttrack.model.expense.RecurringExpenseManager; +import javafx.application.Platform; +import javafx.scene.control.Label; + +class RecurringExpenseCardTest { + + private RecurringExpenseManager recurringExpenseManager; + private int displayedIndex; + + @BeforeEach + public void setUp() { + if (System.getProperty("os.name").toLowerCase().contains("linux")) { + return; + } + recurringExpenseManager = GYM_MEMBERSHIP; + displayedIndex = 1; + } + + @BeforeAll + static void initJfx() throws InterruptedException { + if (System.getProperty("os.name").toLowerCase().contains("linux")) { + return; + } + initJavaFxHelper(); + } + + + @Test + public void testRecurringExpenseCard_validData_success() { + if (System.getProperty("os.name").toLowerCase().contains("linux")) { + return; + } + RecurringExpenseCard recurringExpenseCard = new RecurringExpenseCard( + recurringExpenseManager, displayedIndex); + CompletableFuture future = new CompletableFuture<>(); + Platform.runLater(() -> { + try { + // Test that the category name label is set correctly + Label expenseNameLabel = (Label) recurringExpenseCard.getRoot().lookup("#expenseName"); + assertEquals("Gym Membership", expenseNameLabel.getText()); + + // Test that the index label is set correctly + Label indexLabel = (Label) recurringExpenseCard.getRoot().lookup("#id"); + assertEquals("1. ", indexLabel.getText()); + + // Test that the price label is set correctly + Label priceLabel = (Label) recurringExpenseCard.getRoot().lookup("#price"); + assertEquals("$50.00", priceLabel.getText()); + + // Test that the category label is set correctly + Label categoryLabel = (Label) recurringExpenseCard.getRoot().lookup("#category"); + assertEquals("Fitness", categoryLabel.getText()); + + // Test that the frequency label is set correctly + Label frequencyLabel = (Label) recurringExpenseCard.getRoot().lookup("#frequency"); + assertEquals("Monthly", frequencyLabel.getText()); + future.complete(null); + } catch (AssertionFailedError e) { + future.completeExceptionally(e); + } + }); + try { + future.get(); + } catch (InterruptedException | ExecutionException e) { + fail("Assertion error thrown in Platform.runLater thread: " + e.getMessage()); + } + } + + @Test + public void testEquals_validRecurringExpenseCard_success() { + if (System.getProperty("os.name").toLowerCase().contains("linux")) { + return; + } + RecurringExpenseCard recurringExpenseCard1 = new RecurringExpenseCard( + recurringExpenseManager, displayedIndex); + RecurringExpenseCard recurringExpenseCard2 = new RecurringExpenseCard( + recurringExpenseManager, displayedIndex + 1); + RecurringExpenseCard recurringExpenseCard3 = new RecurringExpenseCard( + recurringExpenseManager, displayedIndex + 1); + CompletableFuture future = new CompletableFuture<>(); + + Platform.runLater(() -> { + try { + assertEquals(recurringExpenseCard1, recurringExpenseCard1); + assertNotEquals(recurringExpenseCard1, recurringExpenseCard2); + assertEquals(recurringExpenseCard2, recurringExpenseCard3); + future.complete(null); + } catch (AssertionFailedError e) { + future.completeExceptionally(e); + } + }); + try { + future.get(); + } catch (InterruptedException | ExecutionException e) { + fail("Assertion error thrown in Platform.runLater thread: " + e.getMessage()); + } + } +} diff --git a/src/test/java/fasttrack/ui/RecurringExpensePanelTest.java b/src/test/java/fasttrack/ui/RecurringExpensePanelTest.java new file mode 100644 index 00000000000..b6b487dccee --- /dev/null +++ b/src/test/java/fasttrack/ui/RecurringExpensePanelTest.java @@ -0,0 +1,95 @@ +package fasttrack.ui; + +import static fasttrack.testutil.TypicalRecurringExpenses.GYM_MEMBERSHIP; +import static fasttrack.testutil.TypicalRecurringExpenses.NETFLIX_SUBSCRIPTION; +import static fasttrack.testutil.TypicalRecurringExpenses.RENT; +import static fasttrack.ui.JavaFxTestHelper.initJavaFxHelper; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentest4j.AssertionFailedError; + +import fasttrack.model.expense.RecurringExpenseManager; +import javafx.application.Platform; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.scene.control.ListView; + + +class RecurringExpensePanelTest { + + private RecurringExpensePanel recurringExpensePanel; + private ObservableList recurringExpenses; + + @BeforeEach + public void setUp() { + if (System.getProperty("os.name").toLowerCase().contains("linux")) { + return; + } + recurringExpenses = FXCollections.observableArrayList(GYM_MEMBERSHIP, NETFLIX_SUBSCRIPTION, RENT); + } + + @BeforeAll + static void initJfx() throws InterruptedException { + if (System.getProperty("os.name").toLowerCase().contains("linux")) { + return; + } + initJavaFxHelper(); + } + + @Test + public void recurringExpenseListView_validExpenses_countEqual() { + if (System.getProperty("os.name").toLowerCase().contains("linux")) { + return; + } + recurringExpensePanel = new RecurringExpensePanel(recurringExpenses); + CompletableFuture future = new CompletableFuture<>(); + Platform.runLater(() -> { + try { + // Test that the number of recurring expenses is correct + ListView recurringExpenseListView = (ListView) recurringExpensePanel + .getRoot().lookup("#recurringExpenseListView"); + assertEquals(recurringExpenses.size(), recurringExpenseListView.getItems().size()); + future.complete(null); + } catch (AssertionFailedError e) { + future.completeExceptionally(e); + } + }); + try { + future.get(); + } catch (InterruptedException | ExecutionException e) { + fail("Assertion error thrown in Platform.runLater thread: " + e.getMessage()); + } + } + + @Test + public void recurringExpenseListView_validExpenses_countZero() { + if (System.getProperty("os.name").toLowerCase().contains("linux")) { + return; + } + recurringExpenses = FXCollections.observableArrayList(); + RecurringExpensePanel recurringExpensePanel = new RecurringExpensePanel(recurringExpenses); + CompletableFuture future = new CompletableFuture<>(); + Platform.runLater(() -> { + try { + ListView recurringExpenseListView = (ListView) recurringExpensePanel + .getRoot().lookup("#recurringExpenseListView"); + assertEquals(0, recurringExpenseListView.getItems().size()); + future.complete(null); + } catch (AssertionFailedError e) { + future.completeExceptionally(e); + } + }); + try { + future.get(); + } catch (InterruptedException | ExecutionException e) { + fail("Assertion error thrown in Platform.runLater thread: " + e.getMessage()); + } + } +} diff --git a/src/test/java/seedu/address/ui/TestFxmlObject.java b/src/test/java/fasttrack/ui/TestFxmlObject.java similarity index 96% rename from src/test/java/seedu/address/ui/TestFxmlObject.java rename to src/test/java/fasttrack/ui/TestFxmlObject.java index 5ecd82656f2..5ef9f5168a8 100644 --- a/src/test/java/seedu/address/ui/TestFxmlObject.java +++ b/src/test/java/fasttrack/ui/TestFxmlObject.java @@ -1,4 +1,4 @@ -package seedu.address.ui; +package fasttrack.ui; import javafx.beans.DefaultProperty; diff --git a/src/test/java/seedu/address/ui/UiPartTest.java b/src/test/java/fasttrack/ui/UiPartTest.java similarity index 97% rename from src/test/java/seedu/address/ui/UiPartTest.java rename to src/test/java/fasttrack/ui/UiPartTest.java index 33d82d911b8..2d46b5a99f4 100644 --- a/src/test/java/seedu/address/ui/UiPartTest.java +++ b/src/test/java/fasttrack/ui/UiPartTest.java @@ -1,8 +1,8 @@ -package seedu.address.ui; +package fasttrack.ui; +import static fasttrack.testutil.Assert.assertThrows; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static seedu.address.testutil.Assert.assertThrows; import java.net.URL; import java.nio.file.Path; @@ -10,8 +10,8 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import fasttrack.MainApp; import javafx.fxml.FXML; -import seedu.address.MainApp; public class UiPartTest { diff --git a/src/test/java/seedu/address/logic/commands/AddCommandIntegrationTest.java b/src/test/java/seedu/address/logic/commands/AddCommandIntegrationTest.java deleted file mode 100644 index cb8714bb055..00000000000 --- a/src/test/java/seedu/address/logic/commands/AddCommandIntegrationTest.java +++ /dev/null @@ -1,45 +0,0 @@ -package seedu.address.logic.commands; - -import static seedu.address.logic.commands.CommandTestUtil.assertCommandFailure; -import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; -import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import seedu.address.model.Model; -import seedu.address.model.ModelManager; -import seedu.address.model.UserPrefs; -import seedu.address.model.person.Person; -import seedu.address.testutil.PersonBuilder; - -/** - * Contains integration tests (interaction with the Model) for {@code AddCommand}. - */ -public class AddCommandIntegrationTest { - - private Model model; - - @BeforeEach - public void setUp() { - model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); - } - - @Test - public void execute_newPerson_success() { - Person validPerson = new PersonBuilder().build(); - - Model expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); - expectedModel.addPerson(validPerson); - - assertCommandSuccess(new AddCommand(validPerson), model, - String.format(AddCommand.MESSAGE_SUCCESS, validPerson), expectedModel); - } - - @Test - public void execute_duplicatePerson_throwsCommandException() { - Person personInList = model.getAddressBook().getPersonList().get(0); - assertCommandFailure(new AddCommand(personInList), model, AddCommand.MESSAGE_DUPLICATE_PERSON); - } - -} diff --git a/src/test/java/seedu/address/logic/commands/AddCommandTest.java b/src/test/java/seedu/address/logic/commands/AddCommandTest.java deleted file mode 100644 index 5865713d5dd..00000000000 --- a/src/test/java/seedu/address/logic/commands/AddCommandTest.java +++ /dev/null @@ -1,194 +0,0 @@ -package seedu.address.logic.commands; - -import static java.util.Objects.requireNonNull; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static seedu.address.testutil.Assert.assertThrows; - -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.function.Predicate; - -import org.junit.jupiter.api.Test; - -import javafx.collections.ObservableList; -import seedu.address.commons.core.GuiSettings; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.model.AddressBook; -import seedu.address.model.Model; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.ReadOnlyUserPrefs; -import seedu.address.model.person.Person; -import seedu.address.testutil.PersonBuilder; - -public class AddCommandTest { - - @Test - public void constructor_nullPerson_throwsNullPointerException() { - assertThrows(NullPointerException.class, () -> new AddCommand(null)); - } - - @Test - public void execute_personAcceptedByModel_addSuccessful() throws Exception { - ModelStubAcceptingPersonAdded modelStub = new ModelStubAcceptingPersonAdded(); - Person validPerson = new PersonBuilder().build(); - - CommandResult commandResult = new AddCommand(validPerson).execute(modelStub); - - assertEquals(String.format(AddCommand.MESSAGE_SUCCESS, validPerson), commandResult.getFeedbackToUser()); - assertEquals(Arrays.asList(validPerson), modelStub.personsAdded); - } - - @Test - public void execute_duplicatePerson_throwsCommandException() { - Person validPerson = new PersonBuilder().build(); - AddCommand addCommand = new AddCommand(validPerson); - ModelStub modelStub = new ModelStubWithPerson(validPerson); - - assertThrows(CommandException.class, AddCommand.MESSAGE_DUPLICATE_PERSON, () -> addCommand.execute(modelStub)); - } - - @Test - public void equals() { - Person alice = new PersonBuilder().withName("Alice").build(); - Person bob = new PersonBuilder().withName("Bob").build(); - AddCommand addAliceCommand = new AddCommand(alice); - AddCommand addBobCommand = new AddCommand(bob); - - // same object -> returns true - assertTrue(addAliceCommand.equals(addAliceCommand)); - - // same values -> returns true - AddCommand addAliceCommandCopy = new AddCommand(alice); - assertTrue(addAliceCommand.equals(addAliceCommandCopy)); - - // different types -> returns false - assertFalse(addAliceCommand.equals(1)); - - // null -> returns false - assertFalse(addAliceCommand.equals(null)); - - // different person -> returns false - assertFalse(addAliceCommand.equals(addBobCommand)); - } - - /** - * A default model stub that have all of the methods failing. - */ - private class ModelStub implements Model { - @Override - public void setUserPrefs(ReadOnlyUserPrefs userPrefs) { - throw new AssertionError("This method should not be called."); - } - - @Override - public ReadOnlyUserPrefs getUserPrefs() { - throw new AssertionError("This method should not be called."); - } - - @Override - public GuiSettings getGuiSettings() { - throw new AssertionError("This method should not be called."); - } - - @Override - public void setGuiSettings(GuiSettings guiSettings) { - throw new AssertionError("This method should not be called."); - } - - @Override - public Path getAddressBookFilePath() { - throw new AssertionError("This method should not be called."); - } - - @Override - public void setAddressBookFilePath(Path addressBookFilePath) { - throw new AssertionError("This method should not be called."); - } - - @Override - public void addPerson(Person person) { - throw new AssertionError("This method should not be called."); - } - - @Override - public void setAddressBook(ReadOnlyAddressBook newData) { - throw new AssertionError("This method should not be called."); - } - - @Override - public ReadOnlyAddressBook getAddressBook() { - throw new AssertionError("This method should not be called."); - } - - @Override - public boolean hasPerson(Person person) { - throw new AssertionError("This method should not be called."); - } - - @Override - public void deletePerson(Person target) { - throw new AssertionError("This method should not be called."); - } - - @Override - public void setPerson(Person target, Person editedPerson) { - throw new AssertionError("This method should not be called."); - } - - @Override - public ObservableList getFilteredPersonList() { - throw new AssertionError("This method should not be called."); - } - - @Override - public void updateFilteredPersonList(Predicate predicate) { - throw new AssertionError("This method should not be called."); - } - } - - /** - * A Model stub that contains a single person. - */ - private class ModelStubWithPerson extends ModelStub { - private final Person person; - - ModelStubWithPerson(Person person) { - requireNonNull(person); - this.person = person; - } - - @Override - public boolean hasPerson(Person person) { - requireNonNull(person); - return this.person.isSamePerson(person); - } - } - - /** - * A Model stub that always accept the person being added. - */ - private class ModelStubAcceptingPersonAdded extends ModelStub { - final ArrayList personsAdded = new ArrayList<>(); - - @Override - public boolean hasPerson(Person person) { - requireNonNull(person); - return personsAdded.stream().anyMatch(person::isSamePerson); - } - - @Override - public void addPerson(Person person) { - requireNonNull(person); - personsAdded.add(person); - } - - @Override - public ReadOnlyAddressBook getAddressBook() { - return new AddressBook(); - } - } - -} diff --git a/src/test/java/seedu/address/logic/commands/ClearCommandTest.java b/src/test/java/seedu/address/logic/commands/ClearCommandTest.java deleted file mode 100644 index 80d9110c03a..00000000000 --- a/src/test/java/seedu/address/logic/commands/ClearCommandTest.java +++ /dev/null @@ -1,32 +0,0 @@ -package seedu.address.logic.commands; - -import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; -import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; - -import org.junit.jupiter.api.Test; - -import seedu.address.model.AddressBook; -import seedu.address.model.Model; -import seedu.address.model.ModelManager; -import seedu.address.model.UserPrefs; - -public class ClearCommandTest { - - @Test - public void execute_emptyAddressBook_success() { - Model model = new ModelManager(); - Model expectedModel = new ModelManager(); - - assertCommandSuccess(new ClearCommand(), model, ClearCommand.MESSAGE_SUCCESS, expectedModel); - } - - @Test - public void execute_nonEmptyAddressBook_success() { - Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); - Model expectedModel = new ModelManager(getTypicalAddressBook(), new UserPrefs()); - expectedModel.setAddressBook(new AddressBook()); - - assertCommandSuccess(new ClearCommand(), model, ClearCommand.MESSAGE_SUCCESS, expectedModel); - } - -} diff --git a/src/test/java/seedu/address/logic/commands/CommandTestUtil.java b/src/test/java/seedu/address/logic/commands/CommandTestUtil.java deleted file mode 100644 index 643a1d08069..00000000000 --- a/src/test/java/seedu/address/logic/commands/CommandTestUtil.java +++ /dev/null @@ -1,128 +0,0 @@ -package seedu.address.logic.commands; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; -import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; -import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; -import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; -import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; -import static seedu.address.testutil.Assert.assertThrows; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import seedu.address.commons.core.index.Index; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.model.AddressBook; -import seedu.address.model.Model; -import seedu.address.model.person.NameContainsKeywordsPredicate; -import seedu.address.model.person.Person; -import seedu.address.testutil.EditPersonDescriptorBuilder; - -/** - * Contains helper methods for testing commands. - */ -public class CommandTestUtil { - - public static final String VALID_NAME_AMY = "Amy Bee"; - public static final String VALID_NAME_BOB = "Bob Choo"; - public static final String VALID_PHONE_AMY = "11111111"; - public static final String VALID_PHONE_BOB = "22222222"; - public static final String VALID_EMAIL_AMY = "amy@example.com"; - public static final String VALID_EMAIL_BOB = "bob@example.com"; - public static final String VALID_ADDRESS_AMY = "Block 312, Amy Street 1"; - public static final String VALID_ADDRESS_BOB = "Block 123, Bobby Street 3"; - public static final String VALID_TAG_HUSBAND = "husband"; - public static final String VALID_TAG_FRIEND = "friend"; - - public static final String NAME_DESC_AMY = " " + PREFIX_NAME + VALID_NAME_AMY; - public static final String NAME_DESC_BOB = " " + PREFIX_NAME + VALID_NAME_BOB; - public static final String PHONE_DESC_AMY = " " + PREFIX_PHONE + VALID_PHONE_AMY; - public static final String PHONE_DESC_BOB = " " + PREFIX_PHONE + VALID_PHONE_BOB; - public static final String EMAIL_DESC_AMY = " " + PREFIX_EMAIL + VALID_EMAIL_AMY; - public static final String EMAIL_DESC_BOB = " " + PREFIX_EMAIL + VALID_EMAIL_BOB; - public static final String ADDRESS_DESC_AMY = " " + PREFIX_ADDRESS + VALID_ADDRESS_AMY; - public static final String ADDRESS_DESC_BOB = " " + PREFIX_ADDRESS + VALID_ADDRESS_BOB; - public static final String TAG_DESC_FRIEND = " " + PREFIX_TAG + VALID_TAG_FRIEND; - public static final String TAG_DESC_HUSBAND = " " + PREFIX_TAG + VALID_TAG_HUSBAND; - - public static final String INVALID_NAME_DESC = " " + PREFIX_NAME + "James&"; // '&' not allowed in names - public static final String INVALID_PHONE_DESC = " " + PREFIX_PHONE + "911a"; // 'a' not allowed in phones - public static final String INVALID_EMAIL_DESC = " " + PREFIX_EMAIL + "bob!yahoo"; // missing '@' symbol - public static final String INVALID_ADDRESS_DESC = " " + PREFIX_ADDRESS; // empty string not allowed for addresses - public static final String INVALID_TAG_DESC = " " + PREFIX_TAG + "hubby*"; // '*' not allowed in tags - - public static final String PREAMBLE_WHITESPACE = "\t \r \n"; - public static final String PREAMBLE_NON_EMPTY = "NonEmptyPreamble"; - - public static final EditCommand.EditPersonDescriptor DESC_AMY; - public static final EditCommand.EditPersonDescriptor DESC_BOB; - - static { - DESC_AMY = new EditPersonDescriptorBuilder().withName(VALID_NAME_AMY) - .withPhone(VALID_PHONE_AMY).withEmail(VALID_EMAIL_AMY).withAddress(VALID_ADDRESS_AMY) - .withTags(VALID_TAG_FRIEND).build(); - DESC_BOB = new EditPersonDescriptorBuilder().withName(VALID_NAME_BOB) - .withPhone(VALID_PHONE_BOB).withEmail(VALID_EMAIL_BOB).withAddress(VALID_ADDRESS_BOB) - .withTags(VALID_TAG_HUSBAND, VALID_TAG_FRIEND).build(); - } - - /** - * Executes the given {@code command}, confirms that
- * - the returned {@link CommandResult} matches {@code expectedCommandResult}
- * - the {@code actualModel} matches {@code expectedModel} - */ - public static void assertCommandSuccess(Command command, Model actualModel, CommandResult expectedCommandResult, - Model expectedModel) { - try { - CommandResult result = command.execute(actualModel); - assertEquals(expectedCommandResult, result); - assertEquals(expectedModel, actualModel); - } catch (CommandException ce) { - throw new AssertionError("Execution of command should not fail.", ce); - } - } - - /** - * Convenience wrapper to {@link #assertCommandSuccess(Command, Model, CommandResult, Model)} - * that takes a string {@code expectedMessage}. - */ - public static void assertCommandSuccess(Command command, Model actualModel, String expectedMessage, - Model expectedModel) { - CommandResult expectedCommandResult = new CommandResult(expectedMessage); - assertCommandSuccess(command, actualModel, expectedCommandResult, expectedModel); - } - - /** - * Executes the given {@code command}, confirms that
- * - a {@code CommandException} is thrown
- * - the CommandException message matches {@code expectedMessage}
- * - the address book, filtered person list and selected person in {@code actualModel} remain unchanged - */ - public static void assertCommandFailure(Command command, Model actualModel, String expectedMessage) { - // we are unable to defensively copy the model for comparison later, so we can - // only do so by copying its components. - AddressBook expectedAddressBook = new AddressBook(actualModel.getAddressBook()); - List expectedFilteredList = new ArrayList<>(actualModel.getFilteredPersonList()); - - assertThrows(CommandException.class, expectedMessage, () -> command.execute(actualModel)); - assertEquals(expectedAddressBook, actualModel.getAddressBook()); - assertEquals(expectedFilteredList, actualModel.getFilteredPersonList()); - } - /** - * Updates {@code model}'s filtered list to show only the person at the given {@code targetIndex} in the - * {@code model}'s address book. - */ - public static void showPersonAtIndex(Model model, Index targetIndex) { - assertTrue(targetIndex.getZeroBased() < model.getFilteredPersonList().size()); - - Person person = model.getFilteredPersonList().get(targetIndex.getZeroBased()); - final String[] splitName = person.getName().fullName.split("\\s+"); - model.updateFilteredPersonList(new NameContainsKeywordsPredicate(Arrays.asList(splitName[0]))); - - assertEquals(1, model.getFilteredPersonList().size()); - } - -} diff --git a/src/test/java/seedu/address/logic/commands/DeleteCommandTest.java b/src/test/java/seedu/address/logic/commands/DeleteCommandTest.java deleted file mode 100644 index 45a8c910ba1..00000000000 --- a/src/test/java/seedu/address/logic/commands/DeleteCommandTest.java +++ /dev/null @@ -1,109 +0,0 @@ -package seedu.address.logic.commands; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static seedu.address.logic.commands.CommandTestUtil.assertCommandFailure; -import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; -import static seedu.address.logic.commands.CommandTestUtil.showPersonAtIndex; -import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; -import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND_PERSON; -import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; - -import org.junit.jupiter.api.Test; - -import seedu.address.commons.core.Messages; -import seedu.address.commons.core.index.Index; -import seedu.address.model.Model; -import seedu.address.model.ModelManager; -import seedu.address.model.UserPrefs; -import seedu.address.model.person.Person; - -/** - * Contains integration tests (interaction with the Model) and unit tests for - * {@code DeleteCommand}. - */ -public class DeleteCommandTest { - - private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); - - @Test - public void execute_validIndexUnfilteredList_success() { - Person personToDelete = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); - DeleteCommand deleteCommand = new DeleteCommand(INDEX_FIRST_PERSON); - - String expectedMessage = String.format(DeleteCommand.MESSAGE_DELETE_PERSON_SUCCESS, personToDelete); - - ModelManager expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); - expectedModel.deletePerson(personToDelete); - - assertCommandSuccess(deleteCommand, model, expectedMessage, expectedModel); - } - - @Test - public void execute_invalidIndexUnfilteredList_throwsCommandException() { - Index outOfBoundIndex = Index.fromOneBased(model.getFilteredPersonList().size() + 1); - DeleteCommand deleteCommand = new DeleteCommand(outOfBoundIndex); - - assertCommandFailure(deleteCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); - } - - @Test - public void execute_validIndexFilteredList_success() { - showPersonAtIndex(model, INDEX_FIRST_PERSON); - - Person personToDelete = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); - DeleteCommand deleteCommand = new DeleteCommand(INDEX_FIRST_PERSON); - - String expectedMessage = String.format(DeleteCommand.MESSAGE_DELETE_PERSON_SUCCESS, personToDelete); - - Model expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); - expectedModel.deletePerson(personToDelete); - showNoPerson(expectedModel); - - assertCommandSuccess(deleteCommand, model, expectedMessage, expectedModel); - } - - @Test - public void execute_invalidIndexFilteredList_throwsCommandException() { - showPersonAtIndex(model, INDEX_FIRST_PERSON); - - Index outOfBoundIndex = INDEX_SECOND_PERSON; - // ensures that outOfBoundIndex is still in bounds of address book list - assertTrue(outOfBoundIndex.getZeroBased() < model.getAddressBook().getPersonList().size()); - - DeleteCommand deleteCommand = new DeleteCommand(outOfBoundIndex); - - assertCommandFailure(deleteCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); - } - - @Test - public void equals() { - DeleteCommand deleteFirstCommand = new DeleteCommand(INDEX_FIRST_PERSON); - DeleteCommand deleteSecondCommand = new DeleteCommand(INDEX_SECOND_PERSON); - - // same object -> returns true - assertTrue(deleteFirstCommand.equals(deleteFirstCommand)); - - // same values -> returns true - DeleteCommand deleteFirstCommandCopy = new DeleteCommand(INDEX_FIRST_PERSON); - assertTrue(deleteFirstCommand.equals(deleteFirstCommandCopy)); - - // different types -> returns false - assertFalse(deleteFirstCommand.equals(1)); - - // null -> returns false - assertFalse(deleteFirstCommand.equals(null)); - - // different person -> returns false - assertFalse(deleteFirstCommand.equals(deleteSecondCommand)); - } - - /** - * Updates {@code model}'s filtered list to show no one. - */ - private void showNoPerson(Model model) { - model.updateFilteredPersonList(p -> false); - - assertTrue(model.getFilteredPersonList().isEmpty()); - } -} diff --git a/src/test/java/seedu/address/logic/commands/EditCommandTest.java b/src/test/java/seedu/address/logic/commands/EditCommandTest.java deleted file mode 100644 index 214c6c2507b..00000000000 --- a/src/test/java/seedu/address/logic/commands/EditCommandTest.java +++ /dev/null @@ -1,173 +0,0 @@ -package seedu.address.logic.commands; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static seedu.address.logic.commands.CommandTestUtil.DESC_AMY; -import static seedu.address.logic.commands.CommandTestUtil.DESC_BOB; -import static seedu.address.logic.commands.CommandTestUtil.VALID_NAME_BOB; -import static seedu.address.logic.commands.CommandTestUtil.VALID_PHONE_BOB; -import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_HUSBAND; -import static seedu.address.logic.commands.CommandTestUtil.assertCommandFailure; -import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; -import static seedu.address.logic.commands.CommandTestUtil.showPersonAtIndex; -import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; -import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND_PERSON; -import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; - -import org.junit.jupiter.api.Test; - -import seedu.address.commons.core.Messages; -import seedu.address.commons.core.index.Index; -import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; -import seedu.address.model.AddressBook; -import seedu.address.model.Model; -import seedu.address.model.ModelManager; -import seedu.address.model.UserPrefs; -import seedu.address.model.person.Person; -import seedu.address.testutil.EditPersonDescriptorBuilder; -import seedu.address.testutil.PersonBuilder; - -/** - * Contains integration tests (interaction with the Model) and unit tests for EditCommand. - */ -public class EditCommandTest { - - private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); - - @Test - public void execute_allFieldsSpecifiedUnfilteredList_success() { - Person editedPerson = new PersonBuilder().build(); - EditPersonDescriptor descriptor = new EditPersonDescriptorBuilder(editedPerson).build(); - EditCommand editCommand = new EditCommand(INDEX_FIRST_PERSON, descriptor); - - String expectedMessage = String.format(EditCommand.MESSAGE_EDIT_PERSON_SUCCESS, editedPerson); - - Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), new UserPrefs()); - expectedModel.setPerson(model.getFilteredPersonList().get(0), editedPerson); - - assertCommandSuccess(editCommand, model, expectedMessage, expectedModel); - } - - @Test - public void execute_someFieldsSpecifiedUnfilteredList_success() { - Index indexLastPerson = Index.fromOneBased(model.getFilteredPersonList().size()); - Person lastPerson = model.getFilteredPersonList().get(indexLastPerson.getZeroBased()); - - PersonBuilder personInList = new PersonBuilder(lastPerson); - Person editedPerson = personInList.withName(VALID_NAME_BOB).withPhone(VALID_PHONE_BOB) - .withTags(VALID_TAG_HUSBAND).build(); - - EditPersonDescriptor descriptor = new EditPersonDescriptorBuilder().withName(VALID_NAME_BOB) - .withPhone(VALID_PHONE_BOB).withTags(VALID_TAG_HUSBAND).build(); - EditCommand editCommand = new EditCommand(indexLastPerson, descriptor); - - String expectedMessage = String.format(EditCommand.MESSAGE_EDIT_PERSON_SUCCESS, editedPerson); - - Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), new UserPrefs()); - expectedModel.setPerson(lastPerson, editedPerson); - - assertCommandSuccess(editCommand, model, expectedMessage, expectedModel); - } - - @Test - public void execute_noFieldSpecifiedUnfilteredList_success() { - EditCommand editCommand = new EditCommand(INDEX_FIRST_PERSON, new EditPersonDescriptor()); - Person editedPerson = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); - - String expectedMessage = String.format(EditCommand.MESSAGE_EDIT_PERSON_SUCCESS, editedPerson); - - Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), new UserPrefs()); - - assertCommandSuccess(editCommand, model, expectedMessage, expectedModel); - } - - @Test - public void execute_filteredList_success() { - showPersonAtIndex(model, INDEX_FIRST_PERSON); - - Person personInFilteredList = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); - Person editedPerson = new PersonBuilder(personInFilteredList).withName(VALID_NAME_BOB).build(); - EditCommand editCommand = new EditCommand(INDEX_FIRST_PERSON, - new EditPersonDescriptorBuilder().withName(VALID_NAME_BOB).build()); - - String expectedMessage = String.format(EditCommand.MESSAGE_EDIT_PERSON_SUCCESS, editedPerson); - - Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), new UserPrefs()); - expectedModel.setPerson(model.getFilteredPersonList().get(0), editedPerson); - - assertCommandSuccess(editCommand, model, expectedMessage, expectedModel); - } - - @Test - public void execute_duplicatePersonUnfilteredList_failure() { - Person firstPerson = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); - EditPersonDescriptor descriptor = new EditPersonDescriptorBuilder(firstPerson).build(); - EditCommand editCommand = new EditCommand(INDEX_SECOND_PERSON, descriptor); - - assertCommandFailure(editCommand, model, EditCommand.MESSAGE_DUPLICATE_PERSON); - } - - @Test - public void execute_duplicatePersonFilteredList_failure() { - showPersonAtIndex(model, INDEX_FIRST_PERSON); - - // edit person in filtered list into a duplicate in address book - Person personInList = model.getAddressBook().getPersonList().get(INDEX_SECOND_PERSON.getZeroBased()); - EditCommand editCommand = new EditCommand(INDEX_FIRST_PERSON, - new EditPersonDescriptorBuilder(personInList).build()); - - assertCommandFailure(editCommand, model, EditCommand.MESSAGE_DUPLICATE_PERSON); - } - - @Test - public void execute_invalidPersonIndexUnfilteredList_failure() { - Index outOfBoundIndex = Index.fromOneBased(model.getFilteredPersonList().size() + 1); - EditPersonDescriptor descriptor = new EditPersonDescriptorBuilder().withName(VALID_NAME_BOB).build(); - EditCommand editCommand = new EditCommand(outOfBoundIndex, descriptor); - - assertCommandFailure(editCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); - } - - /** - * Edit filtered list where index is larger than size of filtered list, - * but smaller than size of address book - */ - @Test - public void execute_invalidPersonIndexFilteredList_failure() { - showPersonAtIndex(model, INDEX_FIRST_PERSON); - Index outOfBoundIndex = INDEX_SECOND_PERSON; - // ensures that outOfBoundIndex is still in bounds of address book list - assertTrue(outOfBoundIndex.getZeroBased() < model.getAddressBook().getPersonList().size()); - - EditCommand editCommand = new EditCommand(outOfBoundIndex, - new EditPersonDescriptorBuilder().withName(VALID_NAME_BOB).build()); - - assertCommandFailure(editCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); - } - - @Test - public void equals() { - final EditCommand standardCommand = new EditCommand(INDEX_FIRST_PERSON, DESC_AMY); - - // same values -> returns true - EditPersonDescriptor copyDescriptor = new EditPersonDescriptor(DESC_AMY); - EditCommand commandWithSameValues = new EditCommand(INDEX_FIRST_PERSON, copyDescriptor); - assertTrue(standardCommand.equals(commandWithSameValues)); - - // same object -> returns true - assertTrue(standardCommand.equals(standardCommand)); - - // null -> returns false - assertFalse(standardCommand.equals(null)); - - // different types -> returns false - assertFalse(standardCommand.equals(new ClearCommand())); - - // different index -> returns false - assertFalse(standardCommand.equals(new EditCommand(INDEX_SECOND_PERSON, DESC_AMY))); - - // different descriptor -> returns false - assertFalse(standardCommand.equals(new EditCommand(INDEX_FIRST_PERSON, DESC_BOB))); - } - -} diff --git a/src/test/java/seedu/address/logic/commands/EditPersonDescriptorTest.java b/src/test/java/seedu/address/logic/commands/EditPersonDescriptorTest.java deleted file mode 100644 index e0288792e72..00000000000 --- a/src/test/java/seedu/address/logic/commands/EditPersonDescriptorTest.java +++ /dev/null @@ -1,58 +0,0 @@ -package seedu.address.logic.commands; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static seedu.address.logic.commands.CommandTestUtil.DESC_AMY; -import static seedu.address.logic.commands.CommandTestUtil.DESC_BOB; -import static seedu.address.logic.commands.CommandTestUtil.VALID_ADDRESS_BOB; -import static seedu.address.logic.commands.CommandTestUtil.VALID_EMAIL_BOB; -import static seedu.address.logic.commands.CommandTestUtil.VALID_NAME_BOB; -import static seedu.address.logic.commands.CommandTestUtil.VALID_PHONE_BOB; -import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_HUSBAND; - -import org.junit.jupiter.api.Test; - -import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; -import seedu.address.testutil.EditPersonDescriptorBuilder; - -public class EditPersonDescriptorTest { - - @Test - public void equals() { - // same values -> returns true - EditPersonDescriptor descriptorWithSameValues = new EditPersonDescriptor(DESC_AMY); - assertTrue(DESC_AMY.equals(descriptorWithSameValues)); - - // same object -> returns true - assertTrue(DESC_AMY.equals(DESC_AMY)); - - // null -> returns false - assertFalse(DESC_AMY.equals(null)); - - // different types -> returns false - assertFalse(DESC_AMY.equals(5)); - - // different values -> returns false - assertFalse(DESC_AMY.equals(DESC_BOB)); - - // different name -> returns false - EditPersonDescriptor editedAmy = new EditPersonDescriptorBuilder(DESC_AMY).withName(VALID_NAME_BOB).build(); - assertFalse(DESC_AMY.equals(editedAmy)); - - // different phone -> returns false - editedAmy = new EditPersonDescriptorBuilder(DESC_AMY).withPhone(VALID_PHONE_BOB).build(); - assertFalse(DESC_AMY.equals(editedAmy)); - - // different email -> returns false - editedAmy = new EditPersonDescriptorBuilder(DESC_AMY).withEmail(VALID_EMAIL_BOB).build(); - assertFalse(DESC_AMY.equals(editedAmy)); - - // different address -> returns false - editedAmy = new EditPersonDescriptorBuilder(DESC_AMY).withAddress(VALID_ADDRESS_BOB).build(); - assertFalse(DESC_AMY.equals(editedAmy)); - - // different tags -> returns false - editedAmy = new EditPersonDescriptorBuilder(DESC_AMY).withTags(VALID_TAG_HUSBAND).build(); - assertFalse(DESC_AMY.equals(editedAmy)); - } -} diff --git a/src/test/java/seedu/address/logic/commands/ExitCommandTest.java b/src/test/java/seedu/address/logic/commands/ExitCommandTest.java deleted file mode 100644 index 9533c473875..00000000000 --- a/src/test/java/seedu/address/logic/commands/ExitCommandTest.java +++ /dev/null @@ -1,20 +0,0 @@ -package seedu.address.logic.commands; - -import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; -import static seedu.address.logic.commands.ExitCommand.MESSAGE_EXIT_ACKNOWLEDGEMENT; - -import org.junit.jupiter.api.Test; - -import seedu.address.model.Model; -import seedu.address.model.ModelManager; - -public class ExitCommandTest { - private Model model = new ModelManager(); - private Model expectedModel = new ModelManager(); - - @Test - public void execute_exit_success() { - CommandResult expectedCommandResult = new CommandResult(MESSAGE_EXIT_ACKNOWLEDGEMENT, false, true); - assertCommandSuccess(new ExitCommand(), model, expectedCommandResult, expectedModel); - } -} diff --git a/src/test/java/seedu/address/logic/commands/FindCommandTest.java b/src/test/java/seedu/address/logic/commands/FindCommandTest.java deleted file mode 100644 index 9b15db28bbb..00000000000 --- a/src/test/java/seedu/address/logic/commands/FindCommandTest.java +++ /dev/null @@ -1,83 +0,0 @@ -package seedu.address.logic.commands; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static seedu.address.commons.core.Messages.MESSAGE_PERSONS_LISTED_OVERVIEW; -import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; -import static seedu.address.testutil.TypicalPersons.CARL; -import static seedu.address.testutil.TypicalPersons.ELLE; -import static seedu.address.testutil.TypicalPersons.FIONA; -import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; - -import java.util.Arrays; -import java.util.Collections; - -import org.junit.jupiter.api.Test; - -import seedu.address.model.Model; -import seedu.address.model.ModelManager; -import seedu.address.model.UserPrefs; -import seedu.address.model.person.NameContainsKeywordsPredicate; - -/** - * Contains integration tests (interaction with the Model) for {@code FindCommand}. - */ -public class FindCommandTest { - private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); - private Model expectedModel = new ModelManager(getTypicalAddressBook(), new UserPrefs()); - - @Test - public void equals() { - NameContainsKeywordsPredicate firstPredicate = - new NameContainsKeywordsPredicate(Collections.singletonList("first")); - NameContainsKeywordsPredicate secondPredicate = - new NameContainsKeywordsPredicate(Collections.singletonList("second")); - - FindCommand findFirstCommand = new FindCommand(firstPredicate); - FindCommand findSecondCommand = new FindCommand(secondPredicate); - - // same object -> returns true - assertTrue(findFirstCommand.equals(findFirstCommand)); - - // same values -> returns true - FindCommand findFirstCommandCopy = new FindCommand(firstPredicate); - assertTrue(findFirstCommand.equals(findFirstCommandCopy)); - - // different types -> returns false - assertFalse(findFirstCommand.equals(1)); - - // null -> returns false - assertFalse(findFirstCommand.equals(null)); - - // different person -> returns false - assertFalse(findFirstCommand.equals(findSecondCommand)); - } - - @Test - public void execute_zeroKeywords_noPersonFound() { - String expectedMessage = String.format(MESSAGE_PERSONS_LISTED_OVERVIEW, 0); - NameContainsKeywordsPredicate predicate = preparePredicate(" "); - FindCommand command = new FindCommand(predicate); - expectedModel.updateFilteredPersonList(predicate); - assertCommandSuccess(command, model, expectedMessage, expectedModel); - assertEquals(Collections.emptyList(), model.getFilteredPersonList()); - } - - @Test - public void execute_multipleKeywords_multiplePersonsFound() { - String expectedMessage = String.format(MESSAGE_PERSONS_LISTED_OVERVIEW, 3); - NameContainsKeywordsPredicate predicate = preparePredicate("Kurz Elle Kunz"); - FindCommand command = new FindCommand(predicate); - expectedModel.updateFilteredPersonList(predicate); - assertCommandSuccess(command, model, expectedMessage, expectedModel); - assertEquals(Arrays.asList(CARL, ELLE, FIONA), model.getFilteredPersonList()); - } - - /** - * Parses {@code userInput} into a {@code NameContainsKeywordsPredicate}. - */ - private NameContainsKeywordsPredicate preparePredicate(String userInput) { - return new NameContainsKeywordsPredicate(Arrays.asList(userInput.split("\\s+"))); - } -} diff --git a/src/test/java/seedu/address/logic/commands/HelpCommandTest.java b/src/test/java/seedu/address/logic/commands/HelpCommandTest.java deleted file mode 100644 index 4904fc4352e..00000000000 --- a/src/test/java/seedu/address/logic/commands/HelpCommandTest.java +++ /dev/null @@ -1,20 +0,0 @@ -package seedu.address.logic.commands; - -import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; -import static seedu.address.logic.commands.HelpCommand.SHOWING_HELP_MESSAGE; - -import org.junit.jupiter.api.Test; - -import seedu.address.model.Model; -import seedu.address.model.ModelManager; - -public class HelpCommandTest { - private Model model = new ModelManager(); - private Model expectedModel = new ModelManager(); - - @Test - public void execute_help_success() { - CommandResult expectedCommandResult = new CommandResult(SHOWING_HELP_MESSAGE, true, false); - assertCommandSuccess(new HelpCommand(), model, expectedCommandResult, expectedModel); - } -} diff --git a/src/test/java/seedu/address/logic/commands/ListCommandTest.java b/src/test/java/seedu/address/logic/commands/ListCommandTest.java deleted file mode 100644 index 435ff1f7275..00000000000 --- a/src/test/java/seedu/address/logic/commands/ListCommandTest.java +++ /dev/null @@ -1,39 +0,0 @@ -package seedu.address.logic.commands; - -import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; -import static seedu.address.logic.commands.CommandTestUtil.showPersonAtIndex; -import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; -import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import seedu.address.model.Model; -import seedu.address.model.ModelManager; -import seedu.address.model.UserPrefs; - -/** - * Contains integration tests (interaction with the Model) and unit tests for ListCommand. - */ -public class ListCommandTest { - - private Model model; - private Model expectedModel; - - @BeforeEach - public void setUp() { - model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); - expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); - } - - @Test - public void execute_listIsNotFiltered_showsSameList() { - assertCommandSuccess(new ListCommand(), model, ListCommand.MESSAGE_SUCCESS, expectedModel); - } - - @Test - public void execute_listIsFiltered_showsEverything() { - showPersonAtIndex(model, INDEX_FIRST_PERSON); - assertCommandSuccess(new ListCommand(), model, ListCommand.MESSAGE_SUCCESS, expectedModel); - } -} diff --git a/src/test/java/seedu/address/logic/parser/AddCommandParserTest.java b/src/test/java/seedu/address/logic/parser/AddCommandParserTest.java deleted file mode 100644 index 5cf487d7ebb..00000000000 --- a/src/test/java/seedu/address/logic/parser/AddCommandParserTest.java +++ /dev/null @@ -1,141 +0,0 @@ -package seedu.address.logic.parser; - -import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; -import static seedu.address.logic.commands.CommandTestUtil.ADDRESS_DESC_AMY; -import static seedu.address.logic.commands.CommandTestUtil.ADDRESS_DESC_BOB; -import static seedu.address.logic.commands.CommandTestUtil.EMAIL_DESC_AMY; -import static seedu.address.logic.commands.CommandTestUtil.EMAIL_DESC_BOB; -import static seedu.address.logic.commands.CommandTestUtil.INVALID_ADDRESS_DESC; -import static seedu.address.logic.commands.CommandTestUtil.INVALID_EMAIL_DESC; -import static seedu.address.logic.commands.CommandTestUtil.INVALID_NAME_DESC; -import static seedu.address.logic.commands.CommandTestUtil.INVALID_PHONE_DESC; -import static seedu.address.logic.commands.CommandTestUtil.INVALID_TAG_DESC; -import static seedu.address.logic.commands.CommandTestUtil.NAME_DESC_AMY; -import static seedu.address.logic.commands.CommandTestUtil.NAME_DESC_BOB; -import static seedu.address.logic.commands.CommandTestUtil.PHONE_DESC_AMY; -import static seedu.address.logic.commands.CommandTestUtil.PHONE_DESC_BOB; -import static seedu.address.logic.commands.CommandTestUtil.PREAMBLE_NON_EMPTY; -import static seedu.address.logic.commands.CommandTestUtil.PREAMBLE_WHITESPACE; -import static seedu.address.logic.commands.CommandTestUtil.TAG_DESC_FRIEND; -import static seedu.address.logic.commands.CommandTestUtil.TAG_DESC_HUSBAND; -import static seedu.address.logic.commands.CommandTestUtil.VALID_ADDRESS_BOB; -import static seedu.address.logic.commands.CommandTestUtil.VALID_EMAIL_BOB; -import static seedu.address.logic.commands.CommandTestUtil.VALID_NAME_BOB; -import static seedu.address.logic.commands.CommandTestUtil.VALID_PHONE_BOB; -import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_FRIEND; -import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_HUSBAND; -import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; -import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; -import static seedu.address.testutil.TypicalPersons.AMY; -import static seedu.address.testutil.TypicalPersons.BOB; - -import org.junit.jupiter.api.Test; - -import seedu.address.logic.commands.AddCommand; -import seedu.address.model.person.Address; -import seedu.address.model.person.Email; -import seedu.address.model.person.Name; -import seedu.address.model.person.Person; -import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; -import seedu.address.testutil.PersonBuilder; - -public class AddCommandParserTest { - private AddCommandParser parser = new AddCommandParser(); - - @Test - public void parse_allFieldsPresent_success() { - Person expectedPerson = new PersonBuilder(BOB).withTags(VALID_TAG_FRIEND).build(); - - // whitespace only preamble - assertParseSuccess(parser, PREAMBLE_WHITESPACE + NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB - + ADDRESS_DESC_BOB + TAG_DESC_FRIEND, new AddCommand(expectedPerson)); - - // multiple names - last name accepted - assertParseSuccess(parser, NAME_DESC_AMY + NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB - + ADDRESS_DESC_BOB + TAG_DESC_FRIEND, new AddCommand(expectedPerson)); - - // multiple phones - last phone accepted - assertParseSuccess(parser, NAME_DESC_BOB + PHONE_DESC_AMY + PHONE_DESC_BOB + EMAIL_DESC_BOB - + ADDRESS_DESC_BOB + TAG_DESC_FRIEND, new AddCommand(expectedPerson)); - - // multiple emails - last email accepted - assertParseSuccess(parser, NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_AMY + EMAIL_DESC_BOB - + ADDRESS_DESC_BOB + TAG_DESC_FRIEND, new AddCommand(expectedPerson)); - - // multiple addresses - last address accepted - assertParseSuccess(parser, NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_AMY - + ADDRESS_DESC_BOB + TAG_DESC_FRIEND, new AddCommand(expectedPerson)); - - // multiple tags - all accepted - Person expectedPersonMultipleTags = new PersonBuilder(BOB).withTags(VALID_TAG_FRIEND, VALID_TAG_HUSBAND) - .build(); - assertParseSuccess(parser, NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB - + TAG_DESC_HUSBAND + TAG_DESC_FRIEND, new AddCommand(expectedPersonMultipleTags)); - } - - @Test - public void parse_optionalFieldsMissing_success() { - // zero tags - Person expectedPerson = new PersonBuilder(AMY).withTags().build(); - assertParseSuccess(parser, NAME_DESC_AMY + PHONE_DESC_AMY + EMAIL_DESC_AMY + ADDRESS_DESC_AMY, - new AddCommand(expectedPerson)); - } - - @Test - public void parse_compulsoryFieldMissing_failure() { - String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE); - - // missing name prefix - assertParseFailure(parser, VALID_NAME_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB, - expectedMessage); - - // missing phone prefix - assertParseFailure(parser, NAME_DESC_BOB + VALID_PHONE_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB, - expectedMessage); - - // missing email prefix - assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + VALID_EMAIL_BOB + ADDRESS_DESC_BOB, - expectedMessage); - - // missing address prefix - assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + VALID_ADDRESS_BOB, - expectedMessage); - - // all prefixes missing - assertParseFailure(parser, VALID_NAME_BOB + VALID_PHONE_BOB + VALID_EMAIL_BOB + VALID_ADDRESS_BOB, - expectedMessage); - } - - @Test - public void parse_invalidValue_failure() { - // invalid name - assertParseFailure(parser, INVALID_NAME_DESC + PHONE_DESC_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB - + TAG_DESC_HUSBAND + TAG_DESC_FRIEND, Name.MESSAGE_CONSTRAINTS); - - // invalid phone - assertParseFailure(parser, NAME_DESC_BOB + INVALID_PHONE_DESC + EMAIL_DESC_BOB + ADDRESS_DESC_BOB - + TAG_DESC_HUSBAND + TAG_DESC_FRIEND, Phone.MESSAGE_CONSTRAINTS); - - // invalid email - assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + INVALID_EMAIL_DESC + ADDRESS_DESC_BOB - + TAG_DESC_HUSBAND + TAG_DESC_FRIEND, Email.MESSAGE_CONSTRAINTS); - - // invalid address - assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + INVALID_ADDRESS_DESC - + TAG_DESC_HUSBAND + TAG_DESC_FRIEND, Address.MESSAGE_CONSTRAINTS); - - // invalid tag - assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB - + INVALID_TAG_DESC + VALID_TAG_FRIEND, Tag.MESSAGE_CONSTRAINTS); - - // two invalid values, only first invalid value reported - assertParseFailure(parser, INVALID_NAME_DESC + PHONE_DESC_BOB + EMAIL_DESC_BOB + INVALID_ADDRESS_DESC, - Name.MESSAGE_CONSTRAINTS); - - // non-empty preamble - assertParseFailure(parser, PREAMBLE_NON_EMPTY + NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB - + ADDRESS_DESC_BOB + TAG_DESC_HUSBAND + TAG_DESC_FRIEND, - String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE)); - } -} diff --git a/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java b/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java deleted file mode 100644 index d9659205b57..00000000000 --- a/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java +++ /dev/null @@ -1,101 +0,0 @@ -package seedu.address.logic.parser; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; -import static seedu.address.commons.core.Messages.MESSAGE_UNKNOWN_COMMAND; -import static seedu.address.testutil.Assert.assertThrows; -import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; - -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; - -import org.junit.jupiter.api.Test; - -import seedu.address.logic.commands.AddCommand; -import seedu.address.logic.commands.ClearCommand; -import seedu.address.logic.commands.DeleteCommand; -import seedu.address.logic.commands.EditCommand; -import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; -import seedu.address.logic.commands.ExitCommand; -import seedu.address.logic.commands.FindCommand; -import seedu.address.logic.commands.HelpCommand; -import seedu.address.logic.commands.ListCommand; -import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.person.NameContainsKeywordsPredicate; -import seedu.address.model.person.Person; -import seedu.address.testutil.EditPersonDescriptorBuilder; -import seedu.address.testutil.PersonBuilder; -import seedu.address.testutil.PersonUtil; - -public class AddressBookParserTest { - - private final AddressBookParser parser = new AddressBookParser(); - - @Test - public void parseCommand_add() throws Exception { - Person person = new PersonBuilder().build(); - AddCommand command = (AddCommand) parser.parseCommand(PersonUtil.getAddCommand(person)); - assertEquals(new AddCommand(person), command); - } - - @Test - public void parseCommand_clear() throws Exception { - assertTrue(parser.parseCommand(ClearCommand.COMMAND_WORD) instanceof ClearCommand); - assertTrue(parser.parseCommand(ClearCommand.COMMAND_WORD + " 3") instanceof ClearCommand); - } - - @Test - public void parseCommand_delete() throws Exception { - DeleteCommand command = (DeleteCommand) parser.parseCommand( - DeleteCommand.COMMAND_WORD + " " + INDEX_FIRST_PERSON.getOneBased()); - assertEquals(new DeleteCommand(INDEX_FIRST_PERSON), command); - } - - @Test - public void parseCommand_edit() throws Exception { - Person person = new PersonBuilder().build(); - EditPersonDescriptor descriptor = new EditPersonDescriptorBuilder(person).build(); - EditCommand command = (EditCommand) parser.parseCommand(EditCommand.COMMAND_WORD + " " - + INDEX_FIRST_PERSON.getOneBased() + " " + PersonUtil.getEditPersonDescriptorDetails(descriptor)); - assertEquals(new EditCommand(INDEX_FIRST_PERSON, descriptor), command); - } - - @Test - public void parseCommand_exit() throws Exception { - assertTrue(parser.parseCommand(ExitCommand.COMMAND_WORD) instanceof ExitCommand); - assertTrue(parser.parseCommand(ExitCommand.COMMAND_WORD + " 3") instanceof ExitCommand); - } - - @Test - public void parseCommand_find() throws Exception { - List keywords = Arrays.asList("foo", "bar", "baz"); - FindCommand command = (FindCommand) parser.parseCommand( - FindCommand.COMMAND_WORD + " " + keywords.stream().collect(Collectors.joining(" "))); - assertEquals(new FindCommand(new NameContainsKeywordsPredicate(keywords)), command); - } - - @Test - public void parseCommand_help() throws Exception { - assertTrue(parser.parseCommand(HelpCommand.COMMAND_WORD) instanceof HelpCommand); - assertTrue(parser.parseCommand(HelpCommand.COMMAND_WORD + " 3") instanceof HelpCommand); - } - - @Test - public void parseCommand_list() throws Exception { - assertTrue(parser.parseCommand(ListCommand.COMMAND_WORD) instanceof ListCommand); - assertTrue(parser.parseCommand(ListCommand.COMMAND_WORD + " 3") instanceof ListCommand); - } - - @Test - public void parseCommand_unrecognisedInput_throwsParseException() { - assertThrows(ParseException.class, String.format(MESSAGE_INVALID_COMMAND_FORMAT, HelpCommand.MESSAGE_USAGE), () - -> parser.parseCommand("")); - } - - @Test - public void parseCommand_unknownCommand_throwsParseException() { - assertThrows(ParseException.class, MESSAGE_UNKNOWN_COMMAND, () -> parser.parseCommand("unknownCommand")); - } -} diff --git a/src/test/java/seedu/address/logic/parser/DeleteCommandParserTest.java b/src/test/java/seedu/address/logic/parser/DeleteCommandParserTest.java deleted file mode 100644 index 27eaec84450..00000000000 --- a/src/test/java/seedu/address/logic/parser/DeleteCommandParserTest.java +++ /dev/null @@ -1,32 +0,0 @@ -package seedu.address.logic.parser; - -import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; -import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; -import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; -import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; - -import org.junit.jupiter.api.Test; - -import seedu.address.logic.commands.DeleteCommand; - -/** - * As we are only doing white-box testing, our test cases do not cover path variations - * outside of the DeleteCommand code. For example, inputs "1" and "1 abc" take the - * same path through the DeleteCommand, and therefore we test only one of them. - * The path variation for those two cases occur inside the ParserUtil, and - * therefore should be covered by the ParserUtilTest. - */ -public class DeleteCommandParserTest { - - private DeleteCommandParser parser = new DeleteCommandParser(); - - @Test - public void parse_validArgs_returnsDeleteCommand() { - assertParseSuccess(parser, "1", new DeleteCommand(INDEX_FIRST_PERSON)); - } - - @Test - public void parse_invalidArgs_throwsParseException() { - assertParseFailure(parser, "a", String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE)); - } -} diff --git a/src/test/java/seedu/address/logic/parser/EditCommandParserTest.java b/src/test/java/seedu/address/logic/parser/EditCommandParserTest.java deleted file mode 100644 index 2ff31522486..00000000000 --- a/src/test/java/seedu/address/logic/parser/EditCommandParserTest.java +++ /dev/null @@ -1,211 +0,0 @@ -package seedu.address.logic.parser; - -import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; -import static seedu.address.logic.commands.CommandTestUtil.ADDRESS_DESC_AMY; -import static seedu.address.logic.commands.CommandTestUtil.ADDRESS_DESC_BOB; -import static seedu.address.logic.commands.CommandTestUtil.EMAIL_DESC_AMY; -import static seedu.address.logic.commands.CommandTestUtil.EMAIL_DESC_BOB; -import static seedu.address.logic.commands.CommandTestUtil.INVALID_ADDRESS_DESC; -import static seedu.address.logic.commands.CommandTestUtil.INVALID_EMAIL_DESC; -import static seedu.address.logic.commands.CommandTestUtil.INVALID_NAME_DESC; -import static seedu.address.logic.commands.CommandTestUtil.INVALID_PHONE_DESC; -import static seedu.address.logic.commands.CommandTestUtil.INVALID_TAG_DESC; -import static seedu.address.logic.commands.CommandTestUtil.NAME_DESC_AMY; -import static seedu.address.logic.commands.CommandTestUtil.PHONE_DESC_AMY; -import static seedu.address.logic.commands.CommandTestUtil.PHONE_DESC_BOB; -import static seedu.address.logic.commands.CommandTestUtil.TAG_DESC_FRIEND; -import static seedu.address.logic.commands.CommandTestUtil.TAG_DESC_HUSBAND; -import static seedu.address.logic.commands.CommandTestUtil.VALID_ADDRESS_AMY; -import static seedu.address.logic.commands.CommandTestUtil.VALID_ADDRESS_BOB; -import static seedu.address.logic.commands.CommandTestUtil.VALID_EMAIL_AMY; -import static seedu.address.logic.commands.CommandTestUtil.VALID_EMAIL_BOB; -import static seedu.address.logic.commands.CommandTestUtil.VALID_NAME_AMY; -import static seedu.address.logic.commands.CommandTestUtil.VALID_PHONE_AMY; -import static seedu.address.logic.commands.CommandTestUtil.VALID_PHONE_BOB; -import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_FRIEND; -import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_HUSBAND; -import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; -import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; -import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; -import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; -import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND_PERSON; -import static seedu.address.testutil.TypicalIndexes.INDEX_THIRD_PERSON; - -import org.junit.jupiter.api.Test; - -import seedu.address.commons.core.index.Index; -import seedu.address.logic.commands.EditCommand; -import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; -import seedu.address.model.person.Address; -import seedu.address.model.person.Email; -import seedu.address.model.person.Name; -import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; -import seedu.address.testutil.EditPersonDescriptorBuilder; - -public class EditCommandParserTest { - - private static final String TAG_EMPTY = " " + PREFIX_TAG; - - private static final String MESSAGE_INVALID_FORMAT = - String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE); - - private EditCommandParser parser = new EditCommandParser(); - - @Test - public void parse_missingParts_failure() { - // no index specified - assertParseFailure(parser, VALID_NAME_AMY, MESSAGE_INVALID_FORMAT); - - // no field specified - assertParseFailure(parser, "1", EditCommand.MESSAGE_NOT_EDITED); - - // no index and no field specified - assertParseFailure(parser, "", MESSAGE_INVALID_FORMAT); - } - - @Test - public void parse_invalidPreamble_failure() { - // negative index - assertParseFailure(parser, "-5" + NAME_DESC_AMY, MESSAGE_INVALID_FORMAT); - - // zero index - assertParseFailure(parser, "0" + NAME_DESC_AMY, MESSAGE_INVALID_FORMAT); - - // invalid arguments being parsed as preamble - assertParseFailure(parser, "1 some random string", MESSAGE_INVALID_FORMAT); - - // invalid prefix being parsed as preamble - assertParseFailure(parser, "1 i/ string", MESSAGE_INVALID_FORMAT); - } - - @Test - public void parse_invalidValue_failure() { - assertParseFailure(parser, "1" + INVALID_NAME_DESC, Name.MESSAGE_CONSTRAINTS); // invalid name - assertParseFailure(parser, "1" + INVALID_PHONE_DESC, Phone.MESSAGE_CONSTRAINTS); // invalid phone - assertParseFailure(parser, "1" + INVALID_EMAIL_DESC, Email.MESSAGE_CONSTRAINTS); // invalid email - assertParseFailure(parser, "1" + INVALID_ADDRESS_DESC, Address.MESSAGE_CONSTRAINTS); // invalid address - assertParseFailure(parser, "1" + INVALID_TAG_DESC, Tag.MESSAGE_CONSTRAINTS); // invalid tag - - // invalid phone followed by valid email - assertParseFailure(parser, "1" + INVALID_PHONE_DESC + EMAIL_DESC_AMY, Phone.MESSAGE_CONSTRAINTS); - - // valid phone followed by invalid phone. The test case for invalid phone followed by valid phone - // is tested at {@code parse_invalidValueFollowedByValidValue_success()} - assertParseFailure(parser, "1" + PHONE_DESC_BOB + INVALID_PHONE_DESC, Phone.MESSAGE_CONSTRAINTS); - - // while parsing {@code PREFIX_TAG} alone will reset the tags of the {@code Person} being edited, - // parsing it together with a valid tag results in error - assertParseFailure(parser, "1" + TAG_DESC_FRIEND + TAG_DESC_HUSBAND + TAG_EMPTY, Tag.MESSAGE_CONSTRAINTS); - assertParseFailure(parser, "1" + TAG_DESC_FRIEND + TAG_EMPTY + TAG_DESC_HUSBAND, Tag.MESSAGE_CONSTRAINTS); - assertParseFailure(parser, "1" + TAG_EMPTY + TAG_DESC_FRIEND + TAG_DESC_HUSBAND, Tag.MESSAGE_CONSTRAINTS); - - // multiple invalid values, but only the first invalid value is captured - assertParseFailure(parser, "1" + INVALID_NAME_DESC + INVALID_EMAIL_DESC + VALID_ADDRESS_AMY + VALID_PHONE_AMY, - Name.MESSAGE_CONSTRAINTS); - } - - @Test - public void parse_allFieldsSpecified_success() { - Index targetIndex = INDEX_SECOND_PERSON; - String userInput = targetIndex.getOneBased() + PHONE_DESC_BOB + TAG_DESC_HUSBAND - + EMAIL_DESC_AMY + ADDRESS_DESC_AMY + NAME_DESC_AMY + TAG_DESC_FRIEND; - - EditPersonDescriptor descriptor = new EditPersonDescriptorBuilder().withName(VALID_NAME_AMY) - .withPhone(VALID_PHONE_BOB).withEmail(VALID_EMAIL_AMY).withAddress(VALID_ADDRESS_AMY) - .withTags(VALID_TAG_HUSBAND, VALID_TAG_FRIEND).build(); - EditCommand expectedCommand = new EditCommand(targetIndex, descriptor); - - assertParseSuccess(parser, userInput, expectedCommand); - } - - @Test - public void parse_someFieldsSpecified_success() { - Index targetIndex = INDEX_FIRST_PERSON; - String userInput = targetIndex.getOneBased() + PHONE_DESC_BOB + EMAIL_DESC_AMY; - - EditPersonDescriptor descriptor = new EditPersonDescriptorBuilder().withPhone(VALID_PHONE_BOB) - .withEmail(VALID_EMAIL_AMY).build(); - EditCommand expectedCommand = new EditCommand(targetIndex, descriptor); - - assertParseSuccess(parser, userInput, expectedCommand); - } - - @Test - public void parse_oneFieldSpecified_success() { - // name - Index targetIndex = INDEX_THIRD_PERSON; - String userInput = targetIndex.getOneBased() + NAME_DESC_AMY; - EditPersonDescriptor descriptor = new EditPersonDescriptorBuilder().withName(VALID_NAME_AMY).build(); - EditCommand expectedCommand = new EditCommand(targetIndex, descriptor); - assertParseSuccess(parser, userInput, expectedCommand); - - // phone - userInput = targetIndex.getOneBased() + PHONE_DESC_AMY; - descriptor = new EditPersonDescriptorBuilder().withPhone(VALID_PHONE_AMY).build(); - expectedCommand = new EditCommand(targetIndex, descriptor); - assertParseSuccess(parser, userInput, expectedCommand); - - // email - userInput = targetIndex.getOneBased() + EMAIL_DESC_AMY; - descriptor = new EditPersonDescriptorBuilder().withEmail(VALID_EMAIL_AMY).build(); - expectedCommand = new EditCommand(targetIndex, descriptor); - assertParseSuccess(parser, userInput, expectedCommand); - - // address - userInput = targetIndex.getOneBased() + ADDRESS_DESC_AMY; - descriptor = new EditPersonDescriptorBuilder().withAddress(VALID_ADDRESS_AMY).build(); - expectedCommand = new EditCommand(targetIndex, descriptor); - assertParseSuccess(parser, userInput, expectedCommand); - - // tags - userInput = targetIndex.getOneBased() + TAG_DESC_FRIEND; - descriptor = new EditPersonDescriptorBuilder().withTags(VALID_TAG_FRIEND).build(); - expectedCommand = new EditCommand(targetIndex, descriptor); - assertParseSuccess(parser, userInput, expectedCommand); - } - - @Test - public void parse_multipleRepeatedFields_acceptsLast() { - Index targetIndex = INDEX_FIRST_PERSON; - String userInput = targetIndex.getOneBased() + PHONE_DESC_AMY + ADDRESS_DESC_AMY + EMAIL_DESC_AMY - + TAG_DESC_FRIEND + PHONE_DESC_AMY + ADDRESS_DESC_AMY + EMAIL_DESC_AMY + TAG_DESC_FRIEND - + PHONE_DESC_BOB + ADDRESS_DESC_BOB + EMAIL_DESC_BOB + TAG_DESC_HUSBAND; - - EditPersonDescriptor descriptor = new EditPersonDescriptorBuilder().withPhone(VALID_PHONE_BOB) - .withEmail(VALID_EMAIL_BOB).withAddress(VALID_ADDRESS_BOB).withTags(VALID_TAG_FRIEND, VALID_TAG_HUSBAND) - .build(); - EditCommand expectedCommand = new EditCommand(targetIndex, descriptor); - - assertParseSuccess(parser, userInput, expectedCommand); - } - - @Test - public void parse_invalidValueFollowedByValidValue_success() { - // no other valid values specified - Index targetIndex = INDEX_FIRST_PERSON; - String userInput = targetIndex.getOneBased() + INVALID_PHONE_DESC + PHONE_DESC_BOB; - EditPersonDescriptor descriptor = new EditPersonDescriptorBuilder().withPhone(VALID_PHONE_BOB).build(); - EditCommand expectedCommand = new EditCommand(targetIndex, descriptor); - assertParseSuccess(parser, userInput, expectedCommand); - - // other valid values specified - userInput = targetIndex.getOneBased() + EMAIL_DESC_BOB + INVALID_PHONE_DESC + ADDRESS_DESC_BOB - + PHONE_DESC_BOB; - descriptor = new EditPersonDescriptorBuilder().withPhone(VALID_PHONE_BOB).withEmail(VALID_EMAIL_BOB) - .withAddress(VALID_ADDRESS_BOB).build(); - expectedCommand = new EditCommand(targetIndex, descriptor); - assertParseSuccess(parser, userInput, expectedCommand); - } - - @Test - public void parse_resetTags_success() { - Index targetIndex = INDEX_THIRD_PERSON; - String userInput = targetIndex.getOneBased() + TAG_EMPTY; - - EditPersonDescriptor descriptor = new EditPersonDescriptorBuilder().withTags().build(); - EditCommand expectedCommand = new EditCommand(targetIndex, descriptor); - - assertParseSuccess(parser, userInput, expectedCommand); - } -} diff --git a/src/test/java/seedu/address/logic/parser/FindCommandParserTest.java b/src/test/java/seedu/address/logic/parser/FindCommandParserTest.java deleted file mode 100644 index 70f4f0e79c4..00000000000 --- a/src/test/java/seedu/address/logic/parser/FindCommandParserTest.java +++ /dev/null @@ -1,34 +0,0 @@ -package seedu.address.logic.parser; - -import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; -import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; -import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; - -import java.util.Arrays; - -import org.junit.jupiter.api.Test; - -import seedu.address.logic.commands.FindCommand; -import seedu.address.model.person.NameContainsKeywordsPredicate; - -public class FindCommandParserTest { - - private FindCommandParser parser = new FindCommandParser(); - - @Test - public void parse_emptyArg_throwsParseException() { - assertParseFailure(parser, " ", String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE)); - } - - @Test - public void parse_validArgs_returnsFindCommand() { - // no leading and trailing whitespaces - FindCommand expectedFindCommand = - new FindCommand(new NameContainsKeywordsPredicate(Arrays.asList("Alice", "Bob"))); - assertParseSuccess(parser, "Alice Bob", expectedFindCommand); - - // multiple whitespaces between keywords - assertParseSuccess(parser, " \n Alice \n \t Bob \t", expectedFindCommand); - } - -} diff --git a/src/test/java/seedu/address/logic/parser/ParserUtilTest.java b/src/test/java/seedu/address/logic/parser/ParserUtilTest.java deleted file mode 100644 index 4256788b1a7..00000000000 --- a/src/test/java/seedu/address/logic/parser/ParserUtilTest.java +++ /dev/null @@ -1,196 +0,0 @@ -package seedu.address.logic.parser; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static seedu.address.logic.parser.ParserUtil.MESSAGE_INVALID_INDEX; -import static seedu.address.testutil.Assert.assertThrows; -import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; - -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; - -import org.junit.jupiter.api.Test; - -import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.person.Address; -import seedu.address.model.person.Email; -import seedu.address.model.person.Name; -import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; - -public class ParserUtilTest { - private static final String INVALID_NAME = "R@chel"; - private static final String INVALID_PHONE = "+651234"; - private static final String INVALID_ADDRESS = " "; - private static final String INVALID_EMAIL = "example.com"; - private static final String INVALID_TAG = "#friend"; - - private static final String VALID_NAME = "Rachel Walker"; - private static final String VALID_PHONE = "123456"; - private static final String VALID_ADDRESS = "123 Main Street #0505"; - private static final String VALID_EMAIL = "rachel@example.com"; - private static final String VALID_TAG_1 = "friend"; - private static final String VALID_TAG_2 = "neighbour"; - - private static final String WHITESPACE = " \t\r\n"; - - @Test - public void parseIndex_invalidInput_throwsParseException() { - assertThrows(ParseException.class, () -> ParserUtil.parseIndex("10 a")); - } - - @Test - public void parseIndex_outOfRangeInput_throwsParseException() { - assertThrows(ParseException.class, MESSAGE_INVALID_INDEX, () - -> ParserUtil.parseIndex(Long.toString(Integer.MAX_VALUE + 1))); - } - - @Test - public void parseIndex_validInput_success() throws Exception { - // No whitespaces - assertEquals(INDEX_FIRST_PERSON, ParserUtil.parseIndex("1")); - - // Leading and trailing whitespaces - assertEquals(INDEX_FIRST_PERSON, ParserUtil.parseIndex(" 1 ")); - } - - @Test - public void parseName_null_throwsNullPointerException() { - assertThrows(NullPointerException.class, () -> ParserUtil.parseName((String) null)); - } - - @Test - public void parseName_invalidValue_throwsParseException() { - assertThrows(ParseException.class, () -> ParserUtil.parseName(INVALID_NAME)); - } - - @Test - public void parseName_validValueWithoutWhitespace_returnsName() throws Exception { - Name expectedName = new Name(VALID_NAME); - assertEquals(expectedName, ParserUtil.parseName(VALID_NAME)); - } - - @Test - public void parseName_validValueWithWhitespace_returnsTrimmedName() throws Exception { - String nameWithWhitespace = WHITESPACE + VALID_NAME + WHITESPACE; - Name expectedName = new Name(VALID_NAME); - assertEquals(expectedName, ParserUtil.parseName(nameWithWhitespace)); - } - - @Test - public void parsePhone_null_throwsNullPointerException() { - assertThrows(NullPointerException.class, () -> ParserUtil.parsePhone((String) null)); - } - - @Test - public void parsePhone_invalidValue_throwsParseException() { - assertThrows(ParseException.class, () -> ParserUtil.parsePhone(INVALID_PHONE)); - } - - @Test - public void parsePhone_validValueWithoutWhitespace_returnsPhone() throws Exception { - Phone expectedPhone = new Phone(VALID_PHONE); - assertEquals(expectedPhone, ParserUtil.parsePhone(VALID_PHONE)); - } - - @Test - public void parsePhone_validValueWithWhitespace_returnsTrimmedPhone() throws Exception { - String phoneWithWhitespace = WHITESPACE + VALID_PHONE + WHITESPACE; - Phone expectedPhone = new Phone(VALID_PHONE); - assertEquals(expectedPhone, ParserUtil.parsePhone(phoneWithWhitespace)); - } - - @Test - public void parseAddress_null_throwsNullPointerException() { - assertThrows(NullPointerException.class, () -> ParserUtil.parseAddress((String) null)); - } - - @Test - public void parseAddress_invalidValue_throwsParseException() { - assertThrows(ParseException.class, () -> ParserUtil.parseAddress(INVALID_ADDRESS)); - } - - @Test - public void parseAddress_validValueWithoutWhitespace_returnsAddress() throws Exception { - Address expectedAddress = new Address(VALID_ADDRESS); - assertEquals(expectedAddress, ParserUtil.parseAddress(VALID_ADDRESS)); - } - - @Test - public void parseAddress_validValueWithWhitespace_returnsTrimmedAddress() throws Exception { - String addressWithWhitespace = WHITESPACE + VALID_ADDRESS + WHITESPACE; - Address expectedAddress = new Address(VALID_ADDRESS); - assertEquals(expectedAddress, ParserUtil.parseAddress(addressWithWhitespace)); - } - - @Test - public void parseEmail_null_throwsNullPointerException() { - assertThrows(NullPointerException.class, () -> ParserUtil.parseEmail((String) null)); - } - - @Test - public void parseEmail_invalidValue_throwsParseException() { - assertThrows(ParseException.class, () -> ParserUtil.parseEmail(INVALID_EMAIL)); - } - - @Test - public void parseEmail_validValueWithoutWhitespace_returnsEmail() throws Exception { - Email expectedEmail = new Email(VALID_EMAIL); - assertEquals(expectedEmail, ParserUtil.parseEmail(VALID_EMAIL)); - } - - @Test - public void parseEmail_validValueWithWhitespace_returnsTrimmedEmail() throws Exception { - String emailWithWhitespace = WHITESPACE + VALID_EMAIL + WHITESPACE; - Email expectedEmail = new Email(VALID_EMAIL); - assertEquals(expectedEmail, ParserUtil.parseEmail(emailWithWhitespace)); - } - - @Test - public void parseTag_null_throwsNullPointerException() { - assertThrows(NullPointerException.class, () -> ParserUtil.parseTag(null)); - } - - @Test - public void parseTag_invalidValue_throwsParseException() { - assertThrows(ParseException.class, () -> ParserUtil.parseTag(INVALID_TAG)); - } - - @Test - public void parseTag_validValueWithoutWhitespace_returnsTag() throws Exception { - Tag expectedTag = new Tag(VALID_TAG_1); - assertEquals(expectedTag, ParserUtil.parseTag(VALID_TAG_1)); - } - - @Test - public void parseTag_validValueWithWhitespace_returnsTrimmedTag() throws Exception { - String tagWithWhitespace = WHITESPACE + VALID_TAG_1 + WHITESPACE; - Tag expectedTag = new Tag(VALID_TAG_1); - assertEquals(expectedTag, ParserUtil.parseTag(tagWithWhitespace)); - } - - @Test - public void parseTags_null_throwsNullPointerException() { - assertThrows(NullPointerException.class, () -> ParserUtil.parseTags(null)); - } - - @Test - public void parseTags_collectionWithInvalidTags_throwsParseException() { - assertThrows(ParseException.class, () -> ParserUtil.parseTags(Arrays.asList(VALID_TAG_1, INVALID_TAG))); - } - - @Test - public void parseTags_emptyCollection_returnsEmptySet() throws Exception { - assertTrue(ParserUtil.parseTags(Collections.emptyList()).isEmpty()); - } - - @Test - public void parseTags_collectionWithValidTags_returnsTagSet() throws Exception { - Set actualTagSet = ParserUtil.parseTags(Arrays.asList(VALID_TAG_1, VALID_TAG_2)); - Set expectedTagSet = new HashSet(Arrays.asList(new Tag(VALID_TAG_1), new Tag(VALID_TAG_2))); - - assertEquals(expectedTagSet, actualTagSet); - } -} diff --git a/src/test/java/seedu/address/model/AddressBookTest.java b/src/test/java/seedu/address/model/AddressBookTest.java deleted file mode 100644 index 87782528ecd..00000000000 --- a/src/test/java/seedu/address/model/AddressBookTest.java +++ /dev/null @@ -1,102 +0,0 @@ -package seedu.address.model; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static seedu.address.logic.commands.CommandTestUtil.VALID_ADDRESS_BOB; -import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_HUSBAND; -import static seedu.address.testutil.Assert.assertThrows; -import static seedu.address.testutil.TypicalPersons.ALICE; -import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; - -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.List; - -import org.junit.jupiter.api.Test; - -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; -import seedu.address.model.person.Person; -import seedu.address.model.person.exceptions.DuplicatePersonException; -import seedu.address.testutil.PersonBuilder; - -public class AddressBookTest { - - private final AddressBook addressBook = new AddressBook(); - - @Test - public void constructor() { - assertEquals(Collections.emptyList(), addressBook.getPersonList()); - } - - @Test - public void resetData_null_throwsNullPointerException() { - assertThrows(NullPointerException.class, () -> addressBook.resetData(null)); - } - - @Test - public void resetData_withValidReadOnlyAddressBook_replacesData() { - AddressBook newData = getTypicalAddressBook(); - addressBook.resetData(newData); - assertEquals(newData, addressBook); - } - - @Test - public void resetData_withDuplicatePersons_throwsDuplicatePersonException() { - // Two persons with the same identity fields - Person editedAlice = new PersonBuilder(ALICE).withAddress(VALID_ADDRESS_BOB).withTags(VALID_TAG_HUSBAND) - .build(); - List newPersons = Arrays.asList(ALICE, editedAlice); - AddressBookStub newData = new AddressBookStub(newPersons); - - assertThrows(DuplicatePersonException.class, () -> addressBook.resetData(newData)); - } - - @Test - public void hasPerson_nullPerson_throwsNullPointerException() { - assertThrows(NullPointerException.class, () -> addressBook.hasPerson(null)); - } - - @Test - public void hasPerson_personNotInAddressBook_returnsFalse() { - assertFalse(addressBook.hasPerson(ALICE)); - } - - @Test - public void hasPerson_personInAddressBook_returnsTrue() { - addressBook.addPerson(ALICE); - assertTrue(addressBook.hasPerson(ALICE)); - } - - @Test - public void hasPerson_personWithSameIdentityFieldsInAddressBook_returnsTrue() { - addressBook.addPerson(ALICE); - Person editedAlice = new PersonBuilder(ALICE).withAddress(VALID_ADDRESS_BOB).withTags(VALID_TAG_HUSBAND) - .build(); - assertTrue(addressBook.hasPerson(editedAlice)); - } - - @Test - public void getPersonList_modifyList_throwsUnsupportedOperationException() { - assertThrows(UnsupportedOperationException.class, () -> addressBook.getPersonList().remove(0)); - } - - /** - * A stub ReadOnlyAddressBook whose persons list can violate interface constraints. - */ - private static class AddressBookStub implements ReadOnlyAddressBook { - private final ObservableList persons = FXCollections.observableArrayList(); - - AddressBookStub(Collection persons) { - this.persons.setAll(persons); - } - - @Override - public ObservableList getPersonList() { - return persons; - } - } - -} diff --git a/src/test/java/seedu/address/model/ModelManagerTest.java b/src/test/java/seedu/address/model/ModelManagerTest.java deleted file mode 100644 index 2cf1418d116..00000000000 --- a/src/test/java/seedu/address/model/ModelManagerTest.java +++ /dev/null @@ -1,132 +0,0 @@ -package seedu.address.model; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; -import static seedu.address.testutil.Assert.assertThrows; -import static seedu.address.testutil.TypicalPersons.ALICE; -import static seedu.address.testutil.TypicalPersons.BENSON; - -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Arrays; - -import org.junit.jupiter.api.Test; - -import seedu.address.commons.core.GuiSettings; -import seedu.address.model.person.NameContainsKeywordsPredicate; -import seedu.address.testutil.AddressBookBuilder; - -public class ModelManagerTest { - - private ModelManager modelManager = new ModelManager(); - - @Test - public void constructor() { - assertEquals(new UserPrefs(), modelManager.getUserPrefs()); - assertEquals(new GuiSettings(), modelManager.getGuiSettings()); - assertEquals(new AddressBook(), new AddressBook(modelManager.getAddressBook())); - } - - @Test - public void setUserPrefs_nullUserPrefs_throwsNullPointerException() { - assertThrows(NullPointerException.class, () -> modelManager.setUserPrefs(null)); - } - - @Test - public void setUserPrefs_validUserPrefs_copiesUserPrefs() { - UserPrefs userPrefs = new UserPrefs(); - userPrefs.setAddressBookFilePath(Paths.get("address/book/file/path")); - userPrefs.setGuiSettings(new GuiSettings(1, 2, 3, 4)); - modelManager.setUserPrefs(userPrefs); - assertEquals(userPrefs, modelManager.getUserPrefs()); - - // Modifying userPrefs should not modify modelManager's userPrefs - UserPrefs oldUserPrefs = new UserPrefs(userPrefs); - userPrefs.setAddressBookFilePath(Paths.get("new/address/book/file/path")); - assertEquals(oldUserPrefs, modelManager.getUserPrefs()); - } - - @Test - public void setGuiSettings_nullGuiSettings_throwsNullPointerException() { - assertThrows(NullPointerException.class, () -> modelManager.setGuiSettings(null)); - } - - @Test - public void setGuiSettings_validGuiSettings_setsGuiSettings() { - GuiSettings guiSettings = new GuiSettings(1, 2, 3, 4); - modelManager.setGuiSettings(guiSettings); - assertEquals(guiSettings, modelManager.getGuiSettings()); - } - - @Test - public void setAddressBookFilePath_nullPath_throwsNullPointerException() { - assertThrows(NullPointerException.class, () -> modelManager.setAddressBookFilePath(null)); - } - - @Test - public void setAddressBookFilePath_validPath_setsAddressBookFilePath() { - Path path = Paths.get("address/book/file/path"); - modelManager.setAddressBookFilePath(path); - assertEquals(path, modelManager.getAddressBookFilePath()); - } - - @Test - public void hasPerson_nullPerson_throwsNullPointerException() { - assertThrows(NullPointerException.class, () -> modelManager.hasPerson(null)); - } - - @Test - public void hasPerson_personNotInAddressBook_returnsFalse() { - assertFalse(modelManager.hasPerson(ALICE)); - } - - @Test - public void hasPerson_personInAddressBook_returnsTrue() { - modelManager.addPerson(ALICE); - assertTrue(modelManager.hasPerson(ALICE)); - } - - @Test - public void getFilteredPersonList_modifyList_throwsUnsupportedOperationException() { - assertThrows(UnsupportedOperationException.class, () -> modelManager.getFilteredPersonList().remove(0)); - } - - @Test - public void equals() { - AddressBook addressBook = new AddressBookBuilder().withPerson(ALICE).withPerson(BENSON).build(); - AddressBook differentAddressBook = new AddressBook(); - UserPrefs userPrefs = new UserPrefs(); - - // same values -> returns true - modelManager = new ModelManager(addressBook, userPrefs); - ModelManager modelManagerCopy = new ModelManager(addressBook, userPrefs); - assertTrue(modelManager.equals(modelManagerCopy)); - - // same object -> returns true - assertTrue(modelManager.equals(modelManager)); - - // null -> returns false - assertFalse(modelManager.equals(null)); - - // different types -> returns false - assertFalse(modelManager.equals(5)); - - // different addressBook -> returns false - assertFalse(modelManager.equals(new ModelManager(differentAddressBook, userPrefs))); - - // different filteredList -> returns false - String[] keywords = ALICE.getName().fullName.split("\\s+"); - modelManager.updateFilteredPersonList(new NameContainsKeywordsPredicate(Arrays.asList(keywords))); - assertFalse(modelManager.equals(new ModelManager(addressBook, userPrefs))); - - // resets modelManager to initial state for upcoming tests - modelManager.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); - - // different userPrefs -> returns false - UserPrefs differentUserPrefs = new UserPrefs(); - differentUserPrefs.setAddressBookFilePath(Paths.get("differentFilePath")); - assertFalse(modelManager.equals(new ModelManager(addressBook, differentUserPrefs))); - } -} diff --git a/src/test/java/seedu/address/model/person/AddressTest.java b/src/test/java/seedu/address/model/person/AddressTest.java deleted file mode 100644 index dcd3be87b3a..00000000000 --- a/src/test/java/seedu/address/model/person/AddressTest.java +++ /dev/null @@ -1,36 +0,0 @@ -package seedu.address.model.person; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static seedu.address.testutil.Assert.assertThrows; - -import org.junit.jupiter.api.Test; - -public class AddressTest { - - @Test - public void constructor_null_throwsNullPointerException() { - assertThrows(NullPointerException.class, () -> new Address(null)); - } - - @Test - public void constructor_invalidAddress_throwsIllegalArgumentException() { - String invalidAddress = ""; - assertThrows(IllegalArgumentException.class, () -> new Address(invalidAddress)); - } - - @Test - public void isValidAddress() { - // null address - assertThrows(NullPointerException.class, () -> Address.isValidAddress(null)); - - // invalid addresses - assertFalse(Address.isValidAddress("")); // empty string - assertFalse(Address.isValidAddress(" ")); // spaces only - - // valid addresses - assertTrue(Address.isValidAddress("Blk 456, Den Road, #01-355")); - assertTrue(Address.isValidAddress("-")); // one character - assertTrue(Address.isValidAddress("Leng Inc; 1234 Market St; San Francisco CA 2349879; USA")); // long address - } -} diff --git a/src/test/java/seedu/address/model/person/EmailTest.java b/src/test/java/seedu/address/model/person/EmailTest.java deleted file mode 100644 index bbcc6c8c98e..00000000000 --- a/src/test/java/seedu/address/model/person/EmailTest.java +++ /dev/null @@ -1,68 +0,0 @@ -package seedu.address.model.person; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static seedu.address.testutil.Assert.assertThrows; - -import org.junit.jupiter.api.Test; - -public class EmailTest { - - @Test - public void constructor_null_throwsNullPointerException() { - assertThrows(NullPointerException.class, () -> new Email(null)); - } - - @Test - public void constructor_invalidEmail_throwsIllegalArgumentException() { - String invalidEmail = ""; - assertThrows(IllegalArgumentException.class, () -> new Email(invalidEmail)); - } - - @Test - public void isValidEmail() { - // null email - assertThrows(NullPointerException.class, () -> Email.isValidEmail(null)); - - // blank email - assertFalse(Email.isValidEmail("")); // empty string - assertFalse(Email.isValidEmail(" ")); // spaces only - - // missing parts - assertFalse(Email.isValidEmail("@example.com")); // missing local part - assertFalse(Email.isValidEmail("peterjackexample.com")); // missing '@' symbol - assertFalse(Email.isValidEmail("peterjack@")); // missing domain name - - // invalid parts - assertFalse(Email.isValidEmail("peterjack@-")); // invalid domain name - assertFalse(Email.isValidEmail("peterjack@exam_ple.com")); // underscore in domain name - assertFalse(Email.isValidEmail("peter jack@example.com")); // spaces in local part - assertFalse(Email.isValidEmail("peterjack@exam ple.com")); // spaces in domain name - assertFalse(Email.isValidEmail(" peterjack@example.com")); // leading space - assertFalse(Email.isValidEmail("peterjack@example.com ")); // trailing space - assertFalse(Email.isValidEmail("peterjack@@example.com")); // double '@' symbol - assertFalse(Email.isValidEmail("peter@jack@example.com")); // '@' symbol in local part - assertFalse(Email.isValidEmail("-peterjack@example.com")); // local part starts with a hyphen - assertFalse(Email.isValidEmail("peterjack-@example.com")); // local part ends with a hyphen - assertFalse(Email.isValidEmail("peter..jack@example.com")); // local part has two consecutive periods - assertFalse(Email.isValidEmail("peterjack@example@com")); // '@' symbol in domain name - assertFalse(Email.isValidEmail("peterjack@.example.com")); // domain name starts with a period - assertFalse(Email.isValidEmail("peterjack@example.com.")); // domain name ends with a period - assertFalse(Email.isValidEmail("peterjack@-example.com")); // domain name starts with a hyphen - assertFalse(Email.isValidEmail("peterjack@example.com-")); // domain name ends with a hyphen - assertFalse(Email.isValidEmail("peterjack@example.c")); // top level domain has less than two chars - - // valid email - assertTrue(Email.isValidEmail("PeterJack_1190@example.com")); // underscore in local part - assertTrue(Email.isValidEmail("PeterJack.1190@example.com")); // period in local part - assertTrue(Email.isValidEmail("PeterJack+1190@example.com")); // '+' symbol in local part - assertTrue(Email.isValidEmail("PeterJack-1190@example.com")); // hyphen in local part - assertTrue(Email.isValidEmail("a@bc")); // minimal - assertTrue(Email.isValidEmail("test@localhost")); // alphabets only - assertTrue(Email.isValidEmail("123@145")); // numeric local part and domain name - assertTrue(Email.isValidEmail("a1+be.d@example1.com")); // mixture of alphanumeric and special characters - assertTrue(Email.isValidEmail("peter_jack@very-very-very-long-example.com")); // long domain name - assertTrue(Email.isValidEmail("if.you.dream.it_you.can.do.it@example.com")); // long local part - assertTrue(Email.isValidEmail("e1234567@u.nus.edu")); // more than one period in domain - } -} diff --git a/src/test/java/seedu/address/model/person/NameContainsKeywordsPredicateTest.java b/src/test/java/seedu/address/model/person/NameContainsKeywordsPredicateTest.java deleted file mode 100644 index f136664e017..00000000000 --- a/src/test/java/seedu/address/model/person/NameContainsKeywordsPredicateTest.java +++ /dev/null @@ -1,75 +0,0 @@ -package seedu.address.model.person; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -import org.junit.jupiter.api.Test; - -import seedu.address.testutil.PersonBuilder; - -public class NameContainsKeywordsPredicateTest { - - @Test - public void equals() { - List firstPredicateKeywordList = Collections.singletonList("first"); - List secondPredicateKeywordList = Arrays.asList("first", "second"); - - NameContainsKeywordsPredicate firstPredicate = new NameContainsKeywordsPredicate(firstPredicateKeywordList); - NameContainsKeywordsPredicate secondPredicate = new NameContainsKeywordsPredicate(secondPredicateKeywordList); - - // same object -> returns true - assertTrue(firstPredicate.equals(firstPredicate)); - - // same values -> returns true - NameContainsKeywordsPredicate firstPredicateCopy = new NameContainsKeywordsPredicate(firstPredicateKeywordList); - assertTrue(firstPredicate.equals(firstPredicateCopy)); - - // different types -> returns false - assertFalse(firstPredicate.equals(1)); - - // null -> returns false - assertFalse(firstPredicate.equals(null)); - - // different person -> returns false - assertFalse(firstPredicate.equals(secondPredicate)); - } - - @Test - public void test_nameContainsKeywords_returnsTrue() { - // One keyword - NameContainsKeywordsPredicate predicate = new NameContainsKeywordsPredicate(Collections.singletonList("Alice")); - assertTrue(predicate.test(new PersonBuilder().withName("Alice Bob").build())); - - // Multiple keywords - predicate = new NameContainsKeywordsPredicate(Arrays.asList("Alice", "Bob")); - assertTrue(predicate.test(new PersonBuilder().withName("Alice Bob").build())); - - // Only one matching keyword - predicate = new NameContainsKeywordsPredicate(Arrays.asList("Bob", "Carol")); - assertTrue(predicate.test(new PersonBuilder().withName("Alice Carol").build())); - - // Mixed-case keywords - predicate = new NameContainsKeywordsPredicate(Arrays.asList("aLIce", "bOB")); - assertTrue(predicate.test(new PersonBuilder().withName("Alice Bob").build())); - } - - @Test - public void test_nameDoesNotContainKeywords_returnsFalse() { - // Zero keywords - NameContainsKeywordsPredicate predicate = new NameContainsKeywordsPredicate(Collections.emptyList()); - assertFalse(predicate.test(new PersonBuilder().withName("Alice").build())); - - // Non-matching keyword - predicate = new NameContainsKeywordsPredicate(Arrays.asList("Carol")); - assertFalse(predicate.test(new PersonBuilder().withName("Alice Bob").build())); - - // Keywords match phone, email and address, but does not match name - predicate = new NameContainsKeywordsPredicate(Arrays.asList("12345", "alice@email.com", "Main", "Street")); - assertFalse(predicate.test(new PersonBuilder().withName("Alice").withPhone("12345") - .withEmail("alice@email.com").withAddress("Main Street").build())); - } -} diff --git a/src/test/java/seedu/address/model/person/NameTest.java b/src/test/java/seedu/address/model/person/NameTest.java deleted file mode 100644 index c9801392874..00000000000 --- a/src/test/java/seedu/address/model/person/NameTest.java +++ /dev/null @@ -1,40 +0,0 @@ -package seedu.address.model.person; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static seedu.address.testutil.Assert.assertThrows; - -import org.junit.jupiter.api.Test; - -public class NameTest { - - @Test - public void constructor_null_throwsNullPointerException() { - assertThrows(NullPointerException.class, () -> new Name(null)); - } - - @Test - public void constructor_invalidName_throwsIllegalArgumentException() { - String invalidName = ""; - assertThrows(IllegalArgumentException.class, () -> new Name(invalidName)); - } - - @Test - public void isValidName() { - // null name - assertThrows(NullPointerException.class, () -> Name.isValidName(null)); - - // invalid name - assertFalse(Name.isValidName("")); // empty string - assertFalse(Name.isValidName(" ")); // spaces only - assertFalse(Name.isValidName("^")); // only non-alphanumeric characters - assertFalse(Name.isValidName("peter*")); // contains non-alphanumeric characters - - // valid name - assertTrue(Name.isValidName("peter jack")); // alphabets only - assertTrue(Name.isValidName("12345")); // numbers only - assertTrue(Name.isValidName("peter the 2nd")); // alphanumeric characters - assertTrue(Name.isValidName("Capital Tan")); // with capital letters - assertTrue(Name.isValidName("David Roger Jackson Ray Jr 2nd")); // long names - } -} diff --git a/src/test/java/seedu/address/model/person/PersonTest.java b/src/test/java/seedu/address/model/person/PersonTest.java deleted file mode 100644 index b29c097cfd4..00000000000 --- a/src/test/java/seedu/address/model/person/PersonTest.java +++ /dev/null @@ -1,91 +0,0 @@ -package seedu.address.model.person; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static seedu.address.logic.commands.CommandTestUtil.VALID_ADDRESS_BOB; -import static seedu.address.logic.commands.CommandTestUtil.VALID_EMAIL_BOB; -import static seedu.address.logic.commands.CommandTestUtil.VALID_NAME_BOB; -import static seedu.address.logic.commands.CommandTestUtil.VALID_PHONE_BOB; -import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_HUSBAND; -import static seedu.address.testutil.Assert.assertThrows; -import static seedu.address.testutil.TypicalPersons.ALICE; -import static seedu.address.testutil.TypicalPersons.BOB; - -import org.junit.jupiter.api.Test; - -import seedu.address.testutil.PersonBuilder; - -public class PersonTest { - - @Test - public void asObservableList_modifyList_throwsUnsupportedOperationException() { - Person person = new PersonBuilder().build(); - assertThrows(UnsupportedOperationException.class, () -> person.getTags().remove(0)); - } - - @Test - public void isSamePerson() { - // same object -> returns true - assertTrue(ALICE.isSamePerson(ALICE)); - - // null -> returns false - assertFalse(ALICE.isSamePerson(null)); - - // same name, all other attributes different -> returns true - Person editedAlice = new PersonBuilder(ALICE).withPhone(VALID_PHONE_BOB).withEmail(VALID_EMAIL_BOB) - .withAddress(VALID_ADDRESS_BOB).withTags(VALID_TAG_HUSBAND).build(); - assertTrue(ALICE.isSamePerson(editedAlice)); - - // different name, all other attributes same -> returns false - editedAlice = new PersonBuilder(ALICE).withName(VALID_NAME_BOB).build(); - assertFalse(ALICE.isSamePerson(editedAlice)); - - // name differs in case, all other attributes same -> returns false - Person editedBob = new PersonBuilder(BOB).withName(VALID_NAME_BOB.toLowerCase()).build(); - assertFalse(BOB.isSamePerson(editedBob)); - - // name has trailing spaces, all other attributes same -> returns false - String nameWithTrailingSpaces = VALID_NAME_BOB + " "; - editedBob = new PersonBuilder(BOB).withName(nameWithTrailingSpaces).build(); - assertFalse(BOB.isSamePerson(editedBob)); - } - - @Test - public void equals() { - // same values -> returns true - Person aliceCopy = new PersonBuilder(ALICE).build(); - assertTrue(ALICE.equals(aliceCopy)); - - // same object -> returns true - assertTrue(ALICE.equals(ALICE)); - - // null -> returns false - assertFalse(ALICE.equals(null)); - - // different type -> returns false - assertFalse(ALICE.equals(5)); - - // different person -> returns false - assertFalse(ALICE.equals(BOB)); - - // different name -> returns false - Person editedAlice = new PersonBuilder(ALICE).withName(VALID_NAME_BOB).build(); - assertFalse(ALICE.equals(editedAlice)); - - // different phone -> returns false - editedAlice = new PersonBuilder(ALICE).withPhone(VALID_PHONE_BOB).build(); - assertFalse(ALICE.equals(editedAlice)); - - // different email -> returns false - editedAlice = new PersonBuilder(ALICE).withEmail(VALID_EMAIL_BOB).build(); - assertFalse(ALICE.equals(editedAlice)); - - // different address -> returns false - editedAlice = new PersonBuilder(ALICE).withAddress(VALID_ADDRESS_BOB).build(); - assertFalse(ALICE.equals(editedAlice)); - - // different tags -> returns false - editedAlice = new PersonBuilder(ALICE).withTags(VALID_TAG_HUSBAND).build(); - assertFalse(ALICE.equals(editedAlice)); - } -} diff --git a/src/test/java/seedu/address/model/person/PhoneTest.java b/src/test/java/seedu/address/model/person/PhoneTest.java deleted file mode 100644 index 8dd52766a5f..00000000000 --- a/src/test/java/seedu/address/model/person/PhoneTest.java +++ /dev/null @@ -1,40 +0,0 @@ -package seedu.address.model.person; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static seedu.address.testutil.Assert.assertThrows; - -import org.junit.jupiter.api.Test; - -public class PhoneTest { - - @Test - public void constructor_null_throwsNullPointerException() { - assertThrows(NullPointerException.class, () -> new Phone(null)); - } - - @Test - public void constructor_invalidPhone_throwsIllegalArgumentException() { - String invalidPhone = ""; - assertThrows(IllegalArgumentException.class, () -> new Phone(invalidPhone)); - } - - @Test - public void isValidPhone() { - // null phone number - assertThrows(NullPointerException.class, () -> Phone.isValidPhone(null)); - - // invalid phone numbers - assertFalse(Phone.isValidPhone("")); // empty string - assertFalse(Phone.isValidPhone(" ")); // spaces only - assertFalse(Phone.isValidPhone("91")); // less than 3 numbers - assertFalse(Phone.isValidPhone("phone")); // non-numeric - assertFalse(Phone.isValidPhone("9011p041")); // alphabets within digits - assertFalse(Phone.isValidPhone("9312 1534")); // spaces within digits - - // valid phone numbers - assertTrue(Phone.isValidPhone("911")); // exactly 3 numbers - assertTrue(Phone.isValidPhone("93121534")); - assertTrue(Phone.isValidPhone("124293842033123")); // long phone numbers - } -} diff --git a/src/test/java/seedu/address/model/person/UniquePersonListTest.java b/src/test/java/seedu/address/model/person/UniquePersonListTest.java deleted file mode 100644 index 1cc5fe9e0fe..00000000000 --- a/src/test/java/seedu/address/model/person/UniquePersonListTest.java +++ /dev/null @@ -1,170 +0,0 @@ -package seedu.address.model.person; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static seedu.address.logic.commands.CommandTestUtil.VALID_ADDRESS_BOB; -import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_HUSBAND; -import static seedu.address.testutil.Assert.assertThrows; -import static seedu.address.testutil.TypicalPersons.ALICE; -import static seedu.address.testutil.TypicalPersons.BOB; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -import org.junit.jupiter.api.Test; - -import seedu.address.model.person.exceptions.DuplicatePersonException; -import seedu.address.model.person.exceptions.PersonNotFoundException; -import seedu.address.testutil.PersonBuilder; - -public class UniquePersonListTest { - - private final UniquePersonList uniquePersonList = new UniquePersonList(); - - @Test - public void contains_nullPerson_throwsNullPointerException() { - assertThrows(NullPointerException.class, () -> uniquePersonList.contains(null)); - } - - @Test - public void contains_personNotInList_returnsFalse() { - assertFalse(uniquePersonList.contains(ALICE)); - } - - @Test - public void contains_personInList_returnsTrue() { - uniquePersonList.add(ALICE); - assertTrue(uniquePersonList.contains(ALICE)); - } - - @Test - public void contains_personWithSameIdentityFieldsInList_returnsTrue() { - uniquePersonList.add(ALICE); - Person editedAlice = new PersonBuilder(ALICE).withAddress(VALID_ADDRESS_BOB).withTags(VALID_TAG_HUSBAND) - .build(); - assertTrue(uniquePersonList.contains(editedAlice)); - } - - @Test - public void add_nullPerson_throwsNullPointerException() { - assertThrows(NullPointerException.class, () -> uniquePersonList.add(null)); - } - - @Test - public void add_duplicatePerson_throwsDuplicatePersonException() { - uniquePersonList.add(ALICE); - assertThrows(DuplicatePersonException.class, () -> uniquePersonList.add(ALICE)); - } - - @Test - public void setPerson_nullTargetPerson_throwsNullPointerException() { - assertThrows(NullPointerException.class, () -> uniquePersonList.setPerson(null, ALICE)); - } - - @Test - public void setPerson_nullEditedPerson_throwsNullPointerException() { - assertThrows(NullPointerException.class, () -> uniquePersonList.setPerson(ALICE, null)); - } - - @Test - public void setPerson_targetPersonNotInList_throwsPersonNotFoundException() { - assertThrows(PersonNotFoundException.class, () -> uniquePersonList.setPerson(ALICE, ALICE)); - } - - @Test - public void setPerson_editedPersonIsSamePerson_success() { - uniquePersonList.add(ALICE); - uniquePersonList.setPerson(ALICE, ALICE); - UniquePersonList expectedUniquePersonList = new UniquePersonList(); - expectedUniquePersonList.add(ALICE); - assertEquals(expectedUniquePersonList, uniquePersonList); - } - - @Test - public void setPerson_editedPersonHasSameIdentity_success() { - uniquePersonList.add(ALICE); - Person editedAlice = new PersonBuilder(ALICE).withAddress(VALID_ADDRESS_BOB).withTags(VALID_TAG_HUSBAND) - .build(); - uniquePersonList.setPerson(ALICE, editedAlice); - UniquePersonList expectedUniquePersonList = new UniquePersonList(); - expectedUniquePersonList.add(editedAlice); - assertEquals(expectedUniquePersonList, uniquePersonList); - } - - @Test - public void setPerson_editedPersonHasDifferentIdentity_success() { - uniquePersonList.add(ALICE); - uniquePersonList.setPerson(ALICE, BOB); - UniquePersonList expectedUniquePersonList = new UniquePersonList(); - expectedUniquePersonList.add(BOB); - assertEquals(expectedUniquePersonList, uniquePersonList); - } - - @Test - public void setPerson_editedPersonHasNonUniqueIdentity_throwsDuplicatePersonException() { - uniquePersonList.add(ALICE); - uniquePersonList.add(BOB); - assertThrows(DuplicatePersonException.class, () -> uniquePersonList.setPerson(ALICE, BOB)); - } - - @Test - public void remove_nullPerson_throwsNullPointerException() { - assertThrows(NullPointerException.class, () -> uniquePersonList.remove(null)); - } - - @Test - public void remove_personDoesNotExist_throwsPersonNotFoundException() { - assertThrows(PersonNotFoundException.class, () -> uniquePersonList.remove(ALICE)); - } - - @Test - public void remove_existingPerson_removesPerson() { - uniquePersonList.add(ALICE); - uniquePersonList.remove(ALICE); - UniquePersonList expectedUniquePersonList = new UniquePersonList(); - assertEquals(expectedUniquePersonList, uniquePersonList); - } - - @Test - public void setPersons_nullUniquePersonList_throwsNullPointerException() { - assertThrows(NullPointerException.class, () -> uniquePersonList.setPersons((UniquePersonList) null)); - } - - @Test - public void setPersons_uniquePersonList_replacesOwnListWithProvidedUniquePersonList() { - uniquePersonList.add(ALICE); - UniquePersonList expectedUniquePersonList = new UniquePersonList(); - expectedUniquePersonList.add(BOB); - uniquePersonList.setPersons(expectedUniquePersonList); - assertEquals(expectedUniquePersonList, uniquePersonList); - } - - @Test - public void setPersons_nullList_throwsNullPointerException() { - assertThrows(NullPointerException.class, () -> uniquePersonList.setPersons((List) null)); - } - - @Test - public void setPersons_list_replacesOwnListWithProvidedList() { - uniquePersonList.add(ALICE); - List personList = Collections.singletonList(BOB); - uniquePersonList.setPersons(personList); - UniquePersonList expectedUniquePersonList = new UniquePersonList(); - expectedUniquePersonList.add(BOB); - assertEquals(expectedUniquePersonList, uniquePersonList); - } - - @Test - public void setPersons_listWithDuplicatePersons_throwsDuplicatePersonException() { - List listWithDuplicatePersons = Arrays.asList(ALICE, ALICE); - assertThrows(DuplicatePersonException.class, () -> uniquePersonList.setPersons(listWithDuplicatePersons)); - } - - @Test - public void asUnmodifiableObservableList_modifyList_throwsUnsupportedOperationException() { - assertThrows(UnsupportedOperationException.class, () - -> uniquePersonList.asUnmodifiableObservableList().remove(0)); - } -} diff --git a/src/test/java/seedu/address/model/tag/TagTest.java b/src/test/java/seedu/address/model/tag/TagTest.java deleted file mode 100644 index 64d07d79ee2..00000000000 --- a/src/test/java/seedu/address/model/tag/TagTest.java +++ /dev/null @@ -1,26 +0,0 @@ -package seedu.address.model.tag; - -import static seedu.address.testutil.Assert.assertThrows; - -import org.junit.jupiter.api.Test; - -public class TagTest { - - @Test - public void constructor_null_throwsNullPointerException() { - assertThrows(NullPointerException.class, () -> new Tag(null)); - } - - @Test - public void constructor_invalidTagName_throwsIllegalArgumentException() { - String invalidTagName = ""; - assertThrows(IllegalArgumentException.class, () -> new Tag(invalidTagName)); - } - - @Test - public void isValidTagName() { - // null tag name - assertThrows(NullPointerException.class, () -> Tag.isValidTagName(null)); - } - -} diff --git a/src/test/java/seedu/address/storage/JsonAdaptedPersonTest.java b/src/test/java/seedu/address/storage/JsonAdaptedPersonTest.java deleted file mode 100644 index 83b11331cdb..00000000000 --- a/src/test/java/seedu/address/storage/JsonAdaptedPersonTest.java +++ /dev/null @@ -1,110 +0,0 @@ -package seedu.address.storage; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static seedu.address.storage.JsonAdaptedPerson.MISSING_FIELD_MESSAGE_FORMAT; -import static seedu.address.testutil.Assert.assertThrows; -import static seedu.address.testutil.TypicalPersons.BENSON; - -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - -import org.junit.jupiter.api.Test; - -import seedu.address.commons.exceptions.IllegalValueException; -import seedu.address.model.person.Address; -import seedu.address.model.person.Email; -import seedu.address.model.person.Name; -import seedu.address.model.person.Phone; - -public class JsonAdaptedPersonTest { - private static final String INVALID_NAME = "R@chel"; - private static final String INVALID_PHONE = "+651234"; - private static final String INVALID_ADDRESS = " "; - private static final String INVALID_EMAIL = "example.com"; - private static final String INVALID_TAG = "#friend"; - - private static final String VALID_NAME = BENSON.getName().toString(); - private static final String VALID_PHONE = BENSON.getPhone().toString(); - private static final String VALID_EMAIL = BENSON.getEmail().toString(); - private static final String VALID_ADDRESS = BENSON.getAddress().toString(); - private static final List VALID_TAGS = BENSON.getTags().stream() - .map(JsonAdaptedTag::new) - .collect(Collectors.toList()); - - @Test - public void toModelType_validPersonDetails_returnsPerson() throws Exception { - JsonAdaptedPerson person = new JsonAdaptedPerson(BENSON); - assertEquals(BENSON, person.toModelType()); - } - - @Test - public void toModelType_invalidName_throwsIllegalValueException() { - JsonAdaptedPerson person = - new JsonAdaptedPerson(INVALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS); - String expectedMessage = Name.MESSAGE_CONSTRAINTS; - assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); - } - - @Test - public void toModelType_nullName_throwsIllegalValueException() { - JsonAdaptedPerson person = new JsonAdaptedPerson(null, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS); - String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Name.class.getSimpleName()); - assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); - } - - @Test - public void toModelType_invalidPhone_throwsIllegalValueException() { - JsonAdaptedPerson person = - new JsonAdaptedPerson(VALID_NAME, INVALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS); - String expectedMessage = Phone.MESSAGE_CONSTRAINTS; - assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); - } - - @Test - public void toModelType_nullPhone_throwsIllegalValueException() { - JsonAdaptedPerson person = new JsonAdaptedPerson(VALID_NAME, null, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS); - String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Phone.class.getSimpleName()); - assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); - } - - @Test - public void toModelType_invalidEmail_throwsIllegalValueException() { - JsonAdaptedPerson person = - new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, INVALID_EMAIL, VALID_ADDRESS, VALID_TAGS); - String expectedMessage = Email.MESSAGE_CONSTRAINTS; - assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); - } - - @Test - public void toModelType_nullEmail_throwsIllegalValueException() { - JsonAdaptedPerson person = new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, null, VALID_ADDRESS, VALID_TAGS); - String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Email.class.getSimpleName()); - assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); - } - - @Test - public void toModelType_invalidAddress_throwsIllegalValueException() { - JsonAdaptedPerson person = - new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, INVALID_ADDRESS, VALID_TAGS); - String expectedMessage = Address.MESSAGE_CONSTRAINTS; - assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); - } - - @Test - public void toModelType_nullAddress_throwsIllegalValueException() { - JsonAdaptedPerson person = new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, null, VALID_TAGS); - String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Address.class.getSimpleName()); - assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); - } - - @Test - public void toModelType_invalidTags_throwsIllegalValueException() { - List invalidTags = new ArrayList<>(VALID_TAGS); - invalidTags.add(new JsonAdaptedTag(INVALID_TAG)); - JsonAdaptedPerson person = - new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, invalidTags); - assertThrows(IllegalValueException.class, person::toModelType); - } - -} diff --git a/src/test/java/seedu/address/storage/JsonAddressBookStorageTest.java b/src/test/java/seedu/address/storage/JsonAddressBookStorageTest.java deleted file mode 100644 index ac3c3af9566..00000000000 --- a/src/test/java/seedu/address/storage/JsonAddressBookStorageTest.java +++ /dev/null @@ -1,110 +0,0 @@ -package seedu.address.storage; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static seedu.address.testutil.Assert.assertThrows; -import static seedu.address.testutil.TypicalPersons.ALICE; -import static seedu.address.testutil.TypicalPersons.HOON; -import static seedu.address.testutil.TypicalPersons.IDA; -import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; - -import java.io.IOException; -import java.nio.file.Path; -import java.nio.file.Paths; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.model.AddressBook; -import seedu.address.model.ReadOnlyAddressBook; - -public class JsonAddressBookStorageTest { - private static final Path TEST_DATA_FOLDER = Paths.get("src", "test", "data", "JsonAddressBookStorageTest"); - - @TempDir - public Path testFolder; - - @Test - public void readAddressBook_nullFilePath_throwsNullPointerException() { - assertThrows(NullPointerException.class, () -> readAddressBook(null)); - } - - private java.util.Optional readAddressBook(String filePath) throws Exception { - return new JsonAddressBookStorage(Paths.get(filePath)).readAddressBook(addToTestDataPathIfNotNull(filePath)); - } - - private Path addToTestDataPathIfNotNull(String prefsFileInTestDataFolder) { - return prefsFileInTestDataFolder != null - ? TEST_DATA_FOLDER.resolve(prefsFileInTestDataFolder) - : null; - } - - @Test - public void read_missingFile_emptyResult() throws Exception { - assertFalse(readAddressBook("NonExistentFile.json").isPresent()); - } - - @Test - public void read_notJsonFormat_exceptionThrown() { - assertThrows(DataConversionException.class, () -> readAddressBook("notJsonFormatAddressBook.json")); - } - - @Test - public void readAddressBook_invalidPersonAddressBook_throwDataConversionException() { - assertThrows(DataConversionException.class, () -> readAddressBook("invalidPersonAddressBook.json")); - } - - @Test - public void readAddressBook_invalidAndValidPersonAddressBook_throwDataConversionException() { - assertThrows(DataConversionException.class, () -> readAddressBook("invalidAndValidPersonAddressBook.json")); - } - - @Test - public void readAndSaveAddressBook_allInOrder_success() throws Exception { - Path filePath = testFolder.resolve("TempAddressBook.json"); - AddressBook original = getTypicalAddressBook(); - JsonAddressBookStorage jsonAddressBookStorage = new JsonAddressBookStorage(filePath); - - // Save in new file and read back - jsonAddressBookStorage.saveAddressBook(original, filePath); - ReadOnlyAddressBook readBack = jsonAddressBookStorage.readAddressBook(filePath).get(); - assertEquals(original, new AddressBook(readBack)); - - // Modify data, overwrite exiting file, and read back - original.addPerson(HOON); - original.removePerson(ALICE); - jsonAddressBookStorage.saveAddressBook(original, filePath); - readBack = jsonAddressBookStorage.readAddressBook(filePath).get(); - assertEquals(original, new AddressBook(readBack)); - - // Save and read without specifying file path - original.addPerson(IDA); - jsonAddressBookStorage.saveAddressBook(original); // file path not specified - readBack = jsonAddressBookStorage.readAddressBook().get(); // file path not specified - assertEquals(original, new AddressBook(readBack)); - - } - - @Test - public void saveAddressBook_nullAddressBook_throwsNullPointerException() { - assertThrows(NullPointerException.class, () -> saveAddressBook(null, "SomeFile.json")); - } - - /** - * Saves {@code addressBook} at the specified {@code filePath}. - */ - private void saveAddressBook(ReadOnlyAddressBook addressBook, String filePath) { - try { - new JsonAddressBookStorage(Paths.get(filePath)) - .saveAddressBook(addressBook, addToTestDataPathIfNotNull(filePath)); - } catch (IOException ioe) { - throw new AssertionError("There should not be an error writing to the file.", ioe); - } - } - - @Test - public void saveAddressBook_nullFilePath_throwsNullPointerException() { - assertThrows(NullPointerException.class, () -> saveAddressBook(new AddressBook(), null)); - } -} diff --git a/src/test/java/seedu/address/storage/JsonSerializableAddressBookTest.java b/src/test/java/seedu/address/storage/JsonSerializableAddressBookTest.java deleted file mode 100644 index 188c9058d20..00000000000 --- a/src/test/java/seedu/address/storage/JsonSerializableAddressBookTest.java +++ /dev/null @@ -1,47 +0,0 @@ -package seedu.address.storage; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static seedu.address.testutil.Assert.assertThrows; - -import java.nio.file.Path; -import java.nio.file.Paths; - -import org.junit.jupiter.api.Test; - -import seedu.address.commons.exceptions.IllegalValueException; -import seedu.address.commons.util.JsonUtil; -import seedu.address.model.AddressBook; -import seedu.address.testutil.TypicalPersons; - -public class JsonSerializableAddressBookTest { - - private static final Path TEST_DATA_FOLDER = Paths.get("src", "test", "data", "JsonSerializableAddressBookTest"); - private static final Path TYPICAL_PERSONS_FILE = TEST_DATA_FOLDER.resolve("typicalPersonsAddressBook.json"); - private static final Path INVALID_PERSON_FILE = TEST_DATA_FOLDER.resolve("invalidPersonAddressBook.json"); - private static final Path DUPLICATE_PERSON_FILE = TEST_DATA_FOLDER.resolve("duplicatePersonAddressBook.json"); - - @Test - public void toModelType_typicalPersonsFile_success() throws Exception { - JsonSerializableAddressBook dataFromFile = JsonUtil.readJsonFile(TYPICAL_PERSONS_FILE, - JsonSerializableAddressBook.class).get(); - AddressBook addressBookFromFile = dataFromFile.toModelType(); - AddressBook typicalPersonsAddressBook = TypicalPersons.getTypicalAddressBook(); - assertEquals(addressBookFromFile, typicalPersonsAddressBook); - } - - @Test - public void toModelType_invalidPersonFile_throwsIllegalValueException() throws Exception { - JsonSerializableAddressBook dataFromFile = JsonUtil.readJsonFile(INVALID_PERSON_FILE, - JsonSerializableAddressBook.class).get(); - assertThrows(IllegalValueException.class, dataFromFile::toModelType); - } - - @Test - public void toModelType_duplicatePersons_throwsIllegalValueException() throws Exception { - JsonSerializableAddressBook dataFromFile = JsonUtil.readJsonFile(DUPLICATE_PERSON_FILE, - JsonSerializableAddressBook.class).get(); - assertThrows(IllegalValueException.class, JsonSerializableAddressBook.MESSAGE_DUPLICATE_PERSON, - dataFromFile::toModelType); - } - -} diff --git a/src/test/java/seedu/address/testutil/AddressBookBuilder.java b/src/test/java/seedu/address/testutil/AddressBookBuilder.java deleted file mode 100644 index d53799fd110..00000000000 --- a/src/test/java/seedu/address/testutil/AddressBookBuilder.java +++ /dev/null @@ -1,34 +0,0 @@ -package seedu.address.testutil; - -import seedu.address.model.AddressBook; -import seedu.address.model.person.Person; - -/** - * A utility class to help with building Addressbook objects. - * Example usage:
- * {@code AddressBook ab = new AddressBookBuilder().withPerson("John", "Doe").build();} - */ -public class AddressBookBuilder { - - private AddressBook addressBook; - - public AddressBookBuilder() { - addressBook = new AddressBook(); - } - - public AddressBookBuilder(AddressBook addressBook) { - this.addressBook = addressBook; - } - - /** - * Adds a new {@code Person} to the {@code AddressBook} that we are building. - */ - public AddressBookBuilder withPerson(Person person) { - addressBook.addPerson(person); - return this; - } - - public AddressBook build() { - return addressBook; - } -} diff --git a/src/test/java/seedu/address/testutil/EditPersonDescriptorBuilder.java b/src/test/java/seedu/address/testutil/EditPersonDescriptorBuilder.java deleted file mode 100644 index 4584bd5044e..00000000000 --- a/src/test/java/seedu/address/testutil/EditPersonDescriptorBuilder.java +++ /dev/null @@ -1,87 +0,0 @@ -package seedu.address.testutil; - -import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; -import seedu.address.model.person.Address; -import seedu.address.model.person.Email; -import seedu.address.model.person.Name; -import seedu.address.model.person.Person; -import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; - -/** - * A utility class to help with building EditPersonDescriptor objects. - */ -public class EditPersonDescriptorBuilder { - - private EditPersonDescriptor descriptor; - - public EditPersonDescriptorBuilder() { - descriptor = new EditPersonDescriptor(); - } - - public EditPersonDescriptorBuilder(EditPersonDescriptor descriptor) { - this.descriptor = new EditPersonDescriptor(descriptor); - } - - /** - * Returns an {@code EditPersonDescriptor} with fields containing {@code person}'s details - */ - public EditPersonDescriptorBuilder(Person person) { - descriptor = new EditPersonDescriptor(); - descriptor.setName(person.getName()); - descriptor.setPhone(person.getPhone()); - descriptor.setEmail(person.getEmail()); - descriptor.setAddress(person.getAddress()); - descriptor.setTags(person.getTags()); - } - - /** - * Sets the {@code Name} of the {@code EditPersonDescriptor} that we are building. - */ - public EditPersonDescriptorBuilder withName(String name) { - descriptor.setName(new Name(name)); - return this; - } - - /** - * Sets the {@code Phone} of the {@code EditPersonDescriptor} that we are building. - */ - public EditPersonDescriptorBuilder withPhone(String phone) { - descriptor.setPhone(new Phone(phone)); - return this; - } - - /** - * Sets the {@code Email} of the {@code EditPersonDescriptor} that we are building. - */ - public EditPersonDescriptorBuilder withEmail(String email) { - descriptor.setEmail(new Email(email)); - return this; - } - - /** - * Sets the {@code Address} of the {@code EditPersonDescriptor} that we are building. - */ - public EditPersonDescriptorBuilder withAddress(String address) { - descriptor.setAddress(new Address(address)); - return this; - } - - /** - * Parses the {@code tags} into a {@code Set} and set it to the {@code EditPersonDescriptor} - * that we are building. - */ - public EditPersonDescriptorBuilder withTags(String... tags) { - Set tagSet = Stream.of(tags).map(Tag::new).collect(Collectors.toSet()); - descriptor.setTags(tagSet); - return this; - } - - public EditPersonDescriptor build() { - return descriptor; - } -} diff --git a/src/test/java/seedu/address/testutil/PersonBuilder.java b/src/test/java/seedu/address/testutil/PersonBuilder.java deleted file mode 100644 index 6be381d39ba..00000000000 --- a/src/test/java/seedu/address/testutil/PersonBuilder.java +++ /dev/null @@ -1,96 +0,0 @@ -package seedu.address.testutil; - -import java.util.HashSet; -import java.util.Set; - -import seedu.address.model.person.Address; -import seedu.address.model.person.Email; -import seedu.address.model.person.Name; -import seedu.address.model.person.Person; -import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; -import seedu.address.model.util.SampleDataUtil; - -/** - * A utility class to help with building Person objects. - */ -public class PersonBuilder { - - public static final String DEFAULT_NAME = "Amy Bee"; - public static final String DEFAULT_PHONE = "85355255"; - public static final String DEFAULT_EMAIL = "amy@gmail.com"; - public static final String DEFAULT_ADDRESS = "123, Jurong West Ave 6, #08-111"; - - private Name name; - private Phone phone; - private Email email; - private Address address; - private Set tags; - - /** - * Creates a {@code PersonBuilder} with the default details. - */ - public PersonBuilder() { - name = new Name(DEFAULT_NAME); - phone = new Phone(DEFAULT_PHONE); - email = new Email(DEFAULT_EMAIL); - address = new Address(DEFAULT_ADDRESS); - tags = new HashSet<>(); - } - - /** - * Initializes the PersonBuilder with the data of {@code personToCopy}. - */ - public PersonBuilder(Person personToCopy) { - name = personToCopy.getName(); - phone = personToCopy.getPhone(); - email = personToCopy.getEmail(); - address = personToCopy.getAddress(); - tags = new HashSet<>(personToCopy.getTags()); - } - - /** - * Sets the {@code Name} of the {@code Person} that we are building. - */ - public PersonBuilder withName(String name) { - this.name = new Name(name); - return this; - } - - /** - * Parses the {@code tags} into a {@code Set} and set it to the {@code Person} that we are building. - */ - public PersonBuilder withTags(String ... tags) { - this.tags = SampleDataUtil.getTagSet(tags); - return this; - } - - /** - * Sets the {@code Address} of the {@code Person} that we are building. - */ - public PersonBuilder withAddress(String address) { - this.address = new Address(address); - return this; - } - - /** - * Sets the {@code Phone} of the {@code Person} that we are building. - */ - public PersonBuilder withPhone(String phone) { - this.phone = new Phone(phone); - return this; - } - - /** - * Sets the {@code Email} of the {@code Person} that we are building. - */ - public PersonBuilder withEmail(String email) { - this.email = new Email(email); - return this; - } - - public Person build() { - return new Person(name, phone, email, address, tags); - } - -} diff --git a/src/test/java/seedu/address/testutil/PersonUtil.java b/src/test/java/seedu/address/testutil/PersonUtil.java deleted file mode 100644 index 90849945183..00000000000 --- a/src/test/java/seedu/address/testutil/PersonUtil.java +++ /dev/null @@ -1,62 +0,0 @@ -package seedu.address.testutil; - -import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; -import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; -import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; -import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; -import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; - -import java.util.Set; - -import seedu.address.logic.commands.AddCommand; -import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; -import seedu.address.model.person.Person; -import seedu.address.model.tag.Tag; - -/** - * A utility class for Person. - */ -public class PersonUtil { - - /** - * Returns an add command string for adding the {@code person}. - */ - public static String getAddCommand(Person person) { - return AddCommand.COMMAND_WORD + " " + getPersonDetails(person); - } - - /** - * Returns the part of command string for the given {@code person}'s details. - */ - public static String getPersonDetails(Person person) { - StringBuilder sb = new StringBuilder(); - sb.append(PREFIX_NAME + person.getName().fullName + " "); - sb.append(PREFIX_PHONE + person.getPhone().value + " "); - sb.append(PREFIX_EMAIL + person.getEmail().value + " "); - sb.append(PREFIX_ADDRESS + person.getAddress().value + " "); - person.getTags().stream().forEach( - s -> sb.append(PREFIX_TAG + s.tagName + " ") - ); - return sb.toString(); - } - - /** - * Returns the part of command string for the given {@code EditPersonDescriptor}'s details. - */ - public static String getEditPersonDescriptorDetails(EditPersonDescriptor descriptor) { - StringBuilder sb = new StringBuilder(); - descriptor.getName().ifPresent(name -> sb.append(PREFIX_NAME).append(name.fullName).append(" ")); - descriptor.getPhone().ifPresent(phone -> sb.append(PREFIX_PHONE).append(phone.value).append(" ")); - descriptor.getEmail().ifPresent(email -> sb.append(PREFIX_EMAIL).append(email.value).append(" ")); - descriptor.getAddress().ifPresent(address -> sb.append(PREFIX_ADDRESS).append(address.value).append(" ")); - if (descriptor.getTags().isPresent()) { - Set tags = descriptor.getTags().get(); - if (tags.isEmpty()) { - sb.append(PREFIX_TAG); - } else { - tags.forEach(s -> sb.append(PREFIX_TAG).append(s.tagName).append(" ")); - } - } - return sb.toString(); - } -} diff --git a/src/test/java/seedu/address/testutil/TypicalPersons.java b/src/test/java/seedu/address/testutil/TypicalPersons.java deleted file mode 100644 index fec76fb7129..00000000000 --- a/src/test/java/seedu/address/testutil/TypicalPersons.java +++ /dev/null @@ -1,76 +0,0 @@ -package seedu.address.testutil; - -import static seedu.address.logic.commands.CommandTestUtil.VALID_ADDRESS_AMY; -import static seedu.address.logic.commands.CommandTestUtil.VALID_ADDRESS_BOB; -import static seedu.address.logic.commands.CommandTestUtil.VALID_EMAIL_AMY; -import static seedu.address.logic.commands.CommandTestUtil.VALID_EMAIL_BOB; -import static seedu.address.logic.commands.CommandTestUtil.VALID_NAME_AMY; -import static seedu.address.logic.commands.CommandTestUtil.VALID_NAME_BOB; -import static seedu.address.logic.commands.CommandTestUtil.VALID_PHONE_AMY; -import static seedu.address.logic.commands.CommandTestUtil.VALID_PHONE_BOB; -import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_FRIEND; -import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_HUSBAND; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import seedu.address.model.AddressBook; -import seedu.address.model.person.Person; - -/** - * A utility class containing a list of {@code Person} objects to be used in tests. - */ -public class TypicalPersons { - - public static final Person ALICE = new PersonBuilder().withName("Alice Pauline") - .withAddress("123, Jurong West Ave 6, #08-111").withEmail("alice@example.com") - .withPhone("94351253") - .withTags("friends").build(); - public static final Person BENSON = new PersonBuilder().withName("Benson Meier") - .withAddress("311, Clementi Ave 2, #02-25") - .withEmail("johnd@example.com").withPhone("98765432") - .withTags("owesMoney", "friends").build(); - public static final Person CARL = new PersonBuilder().withName("Carl Kurz").withPhone("95352563") - .withEmail("heinz@example.com").withAddress("wall street").build(); - public static final Person DANIEL = new PersonBuilder().withName("Daniel Meier").withPhone("87652533") - .withEmail("cornelia@example.com").withAddress("10th street").withTags("friends").build(); - public static final Person ELLE = new PersonBuilder().withName("Elle Meyer").withPhone("9482224") - .withEmail("werner@example.com").withAddress("michegan ave").build(); - public static final Person FIONA = new PersonBuilder().withName("Fiona Kunz").withPhone("9482427") - .withEmail("lydia@example.com").withAddress("little tokyo").build(); - public static final Person GEORGE = new PersonBuilder().withName("George Best").withPhone("9482442") - .withEmail("anna@example.com").withAddress("4th street").build(); - - // Manually added - public static final Person HOON = new PersonBuilder().withName("Hoon Meier").withPhone("8482424") - .withEmail("stefan@example.com").withAddress("little india").build(); - public static final Person IDA = new PersonBuilder().withName("Ida Mueller").withPhone("8482131") - .withEmail("hans@example.com").withAddress("chicago ave").build(); - - // Manually added - Person's details found in {@code CommandTestUtil} - public static final Person AMY = new PersonBuilder().withName(VALID_NAME_AMY).withPhone(VALID_PHONE_AMY) - .withEmail(VALID_EMAIL_AMY).withAddress(VALID_ADDRESS_AMY).withTags(VALID_TAG_FRIEND).build(); - public static final Person BOB = new PersonBuilder().withName(VALID_NAME_BOB).withPhone(VALID_PHONE_BOB) - .withEmail(VALID_EMAIL_BOB).withAddress(VALID_ADDRESS_BOB).withTags(VALID_TAG_HUSBAND, VALID_TAG_FRIEND) - .build(); - - public static final String KEYWORD_MATCHING_MEIER = "Meier"; // A keyword that matches MEIER - - private TypicalPersons() {} // prevents instantiation - - /** - * Returns an {@code AddressBook} with all the typical persons. - */ - public static AddressBook getTypicalAddressBook() { - AddressBook ab = new AddressBook(); - for (Person person : getTypicalPersons()) { - ab.addPerson(person); - } - return ab; - } - - public static List getTypicalPersons() { - return new ArrayList<>(Arrays.asList(ALICE, BENSON, CARL, DANIEL, ELLE, FIONA, GEORGE)); - } -} diff --git a/src/test/resources/view/UiPartTest/validFile.fxml b/src/test/resources/view/UiPartTest/validFile.fxml index bab836af0db..3721505c497 100644 --- a/src/test/resources/view/UiPartTest/validFile.fxml +++ b/src/test/resources/view/UiPartTest/validFile.fxml @@ -1,4 +1,4 @@ - + Hello World! diff --git a/src/test/resources/view/UiPartTest/validFileWithFxRoot.fxml b/src/test/resources/view/UiPartTest/validFileWithFxRoot.fxml index 151e09ce926..a1687ac2b29 100644 --- a/src/test/resources/view/UiPartTest/validFileWithFxRoot.fxml +++ b/src/test/resources/view/UiPartTest/validFileWithFxRoot.fxml @@ -1,6 +1,6 @@ - Hello World!