diff --git a/.gitignore b/.gitignore index 823d175eb670..c8e31b0cdb8a 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,7 @@ lib/* *.log *.log.* *.csv -config.json +/config.json src/test/data/sandbox/ preferences.json .DS_Store diff --git a/ENTRIES_ALL.xlsx b/ENTRIES_ALL.xlsx new file mode 100644 index 000000000000..fa34ff42113d Binary files /dev/null and b/ENTRIES_ALL.xlsx differ diff --git a/README.adoc b/README.adoc index fa50028b99d0..008383ae8c8d 100644 --- a/README.adoc +++ b/README.adoc @@ -1,7 +1,7 @@ -= Address Book (Level 4) += Budgeter ifdef::env-github,env-browser[:relfileprefix: docs/] -https://travis-ci.org/se-edu/addressbook-level4[image:https://travis-ci.org/se-edu/addressbook-level4.svg?branch=master[Build Status]] +https://travis-ci.org/cs2113-ay1819s2-t11-3/main[image:https://travis-ci.org/cs2113-ay1819s2-t11-3/main.svg?branch=master[Build Status]] https://ci.appveyor.com/project/damithc/addressbook-level4[image:https://ci.appveyor.com/api/projects/status/3boko2x2vr5cc3w2?svg=true[Build status]] https://coveralls.io/github/se-edu/addressbook-level4?branch=master[image:https://coveralls.io/repos/github/se-edu/addressbook-level4/badge.svg?branch=master[Coverage Status]] https://www.codacy.com/app/damith/addressbook-level4?utm_source=github.com&utm_medium=referral&utm_content=se-edu/addressbook-level4&utm_campaign=Badge_Grade[image:https://api.codacy.com/project/badge/Grade/fc0b7775cf7f4fdeaf08776f3d8e364a[Codacy Badge]] @@ -14,13 +14,29 @@ ifndef::env-github[] image::images/Ui.png[width="600"] endif::[] -* This is a desktop Address Book application. It has a GUI but most of the user interactions happen using a CLI (Command Line Interface). -* It is a Java sample application intended for students learning Software Engineering while using Java as the main programming language. -* It is *written in OOP fashion*. It provides a *reasonably well-written* code example that is *significantly bigger* (around 6 KLoC)than what students usually write in beginner-level SE modules. -* What's different from https://github.com/se-edu/addressbook-level3[level 3]: -** A more sophisticated GUI that includes a list panel and an in-built Browser. -** More test cases, including automated GUI testing. -** Support for _Build Automation_ using Gradle and for _Continuous Integration_ using Travis CI. +* This is a desktop Financial Planner application. It has a GUI but most of the user interactions happen using a CLI (Command Line Interface). + +*This app is mainly for users who want to:* + +* Understand and reconcile their financial entries to make better financial decisions +* Organise their financial information using mostly CLI +* Have a visual representation of their financial data +* Store and retrieve their financial data efficiently + +*This app is mainly written in an OOP fashion.* + + +*What’s good about this project:* + +* The app has a sophisticated GUI that includes a list panel, a table panel and also report panels + +* The app has the ability to show detailed visual representations of the financial spendings and income data stored in the application. + +* The app has suggestive command UI built in to aid in the typing. + +* Contains many test cases, including automated GUI testing. + +* Supports for Build Automation using Gradle and for Continuous Integration using Travis CI. + == Site Map @@ -34,6 +50,7 @@ endif::[] * Some parts of this sample application were inspired by the excellent http://code.makery.ch/library/javafx-8-tutorial/[Java FX tutorial] by _Marco Jakob_. +* This application is modified from AddressBook-Level4 project created by SE-EDU initiative at https://github.com/se-edu/ * Libraries used: https://github.com/TestFX/TestFX[TextFX], https://github.com/FasterXML/jackson[Jackson], https://github.com/google/guava[Guava], https://github.com/junit-team/junit5[JUnit5] == Licence : link:LICENSE[MIT] diff --git a/_reposense/config.json b/_reposense/config.json new file mode 100644 index 000000000000..dd265a814cad --- /dev/null +++ b/_reposense/config.json @@ -0,0 +1,25 @@ +{ + "authors": + [ + { + "githubId": "frankquekch", + "displayName": "DOM...HAO", + "authorNames": ["frankquekch"] + }, + { + "githubId": "jacobhan", + "displayName": "HAN...WOO", + "authorNames": ["jacobhan", "Jacob Han"] + }, + { + "githubId": "ngkaicong", + "displayName": "NG ...ONG", + "authorNames": ["ngkaicong"] + }, + { + "githubId": "yushao2", + "displayName": "PAN...HAO", + "authorNames": ["yushao2"] + } + ] +} diff --git a/build.gradle b/build.gradle index 4f2949b6e774..1d472f26a0c7 100644 --- a/build.gradle +++ b/build.gradle @@ -12,6 +12,8 @@ plugins { id 'com.github.johnrengelman.shadow' version '2.0.3' id 'org.asciidoctor.convert' version '1.5.6' id 'application' + id 'org.jetbrains.kotlin.jvm' version '1.3.21' + //id 'org.apache.poi' } if (JavaVersion.current() == JavaVersion.VERSION_1_10 @@ -28,7 +30,7 @@ if (JavaVersion.current() == JavaVersion.VERSION_1_10 } // Specifies the entry point of the application -mainClassName = 'seedu.address.MainApp' +mainClassName = 'seedu.budgeteer.MainApp' sourceCompatibility = JavaVersion.VERSION_1_9 targetCompatibility = JavaVersion.VERSION_1_9 @@ -55,12 +57,21 @@ test { } dependencies { + + compile 'org.json:json:20171018' + compile 'com.googlecode.json-simple:json-simple:1.1.1' + String testFxVersion = '4.0.12-alpha' String jUnitVersion = '5.1.0' implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.7.0' implementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: '2.7.4' implementation group: 'com.google.guava', name: 'guava', version: '19.0' + implementation group: 'javax.xml.bind', name: 'jaxb-api', version: '2.2.8' + implementation group: 'com.sun.xml.bind', name: 'jaxb-impl', version: '2.3.0' + implementation group: 'com.sun.xml.bind', name: 'jaxb-core', version: '2.3.0' + implementation group: 'javax.activation', name: 'activation', version: '1.1.1' + testImplementation group: 'junit', name: 'junit', version: '4.12' testImplementation group: 'org.testfx', name: 'testfx-core', version: testFxVersion, { @@ -74,6 +85,31 @@ dependencies { testRuntimeOnly group: 'org.testfx', name: 'openjfx-monocle', version: 'jdk-9+181' testRuntimeOnly group:'org.junit.vintage', name:'junit-vintage-engine', version: jUnitVersion testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: jUnitVersion + compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8" + + // https://mvnrepository.com/artifact/org.apache.poi/poi + compile group: 'org.apache.poi', name: 'poi', version: '3.17' + + // https://mvnrepository.com/artifact/org.apache.poi/poi-ooxml + compile group: 'org.apache.poi', name: 'poi-ooxml', version: '3.17' + + // https://mvnrepository.com/artifact/org.apache.poi/poi-scratchpad + compile group: 'org.apache.poi', name: 'poi-scratchpad', version: '3.17' + + // https://mvnrepository.com/artifact/org.apache.poi/poi-ooxml-schemas + compile group: 'org.apache.poi', name: 'poi-ooxml-schemas', version: '3.17' + + // https://mvnrepository.com/artifact/org.apache.poi/ooxml-schemas + compile group: 'org.apache.poi', name: 'ooxml-schemas', version: '1.1' + + // https://mvnrepository.com/artifact/org.apache.poi/poi-excelant + compile group: 'org.apache.poi', name: 'poi-excelant', version: '3.17' + + // https://mvnrepository.com/artifact/org.apache.poi/poi-contrib + compile group: 'org.apache.poi', name: 'poi-contrib', version: '3.6' + + // https://mvnrepository.com/artifact/org.apache.poi/poi-examples + compile group: 'org.apache.poi', name: 'poi-examples', version: '3.9' } shadowJar { @@ -152,16 +188,16 @@ test { } if (runNonGuiTests) { - test.include 'seedu/address/**' + test.include 'seedu/budgeteer/**' } if (runGuiTests) { test.include 'systemtests/**' - test.include 'seedu/address/ui/**' + test.include 'seedu/budgeteer/ui/**' } if (!runGuiTests) { - test.exclude 'seedu/address/ui/**' + test.exclude 'seedu/budgeteer/ui/**' } } } @@ -202,8 +238,8 @@ asciidoctor { idprefix: '', // for compatibility with GitHub preview idseparator: '-', 'site-root': "${sourceDir}", // must be the same as sourceDir, do not modify - 'site-name': 'AddressBook-Level4', - 'site-githuburl': 'https://github.com/se-edu/addressbook-level4', + 'site-name': 'Financial Savior', + 'site-githuburl': 'https://github.com/cs2113-ay1819s2-t11-3/main', 'site-seedu': true, // delete this line if your project is not a fork (not a SE-EDU project) ] @@ -235,3 +271,13 @@ deployOfflineDocs.dependsOn asciidoctor processResources.dependsOn deployOfflineDocs defaultTasks 'clean', 'headless', 'allTests', 'coverage', 'asciidoctor' +compileKotlin { + kotlinOptions { + jvmTarget = "1.8" + } +} +compileTestKotlin { + kotlinOptions { + jvmTarget = "1.8" + } +} diff --git a/docs/AboutUs.adoc b/docs/AboutUs.adoc index e647ed1e715a..28d5a7e4fcb7 100644 --- a/docs/AboutUs.adoc +++ b/docs/AboutUs.adoc @@ -4,53 +4,52 @@ :imagesDir: images :stylesDir: stylesheets -AddressBook - Level 4 was developed by the https://se-edu.github.io/docs/Team.html[se-edu] team. + -_{The dummy content given below serves as a placeholder to be used by future forks of the project.}_ + -{empty} + +Budgeteer was developed by T11/3. + We are a team based in the http://www.comp.nus.edu.sg[School of Computing, National University of Singapore]. == Project Team -=== John Doe -image::damithc.jpg[width="150", align="left"] -{empty}[http://www.comp.nus.edu.sg/~damithch[homepage]] [https://github.com/damithc[github]] [<>] - -Role: Project Advisor +=== Pang Yu Shao +image::yushao2.png[width="150", align="left"] +{empty}[https://github.com/yushao2[github]] +{empty}[https://cs2113-ay1819s2-t11-3.github.io/main/team/yushao.html[portfolio]] -''' +Role: Developer, QA + -=== John Roe -image::lejolly.jpg[width="150", align="left"] -{empty}[http://github.com/lejolly[github]] [<>] - -Role: Team Lead + -Responsibilities: UI ''' -=== Johnny Doe -image::yijinl.jpg[width="150", align="left"] -{empty}[http://github.com/yijinl[github]] [<>] +=== Dominic +image::frankquekch.png[width="150", align="left"] +{empty}[http://github.com/frankquekch[github]] +{empty}[https://cs2113-ay1819s2-t11-3.github.io/main/team/dominic.html[portfolio]] + Role: Developer + -Responsibilities: Data + ''' +//@@author ngkaicong -=== Johnny Roe -image::m133225.jpg[width="150", align="left"] -{empty}[http://github.com/m133225[github]] [<>] +=== Ng Kai Cong +image::ngkaicong.png[width="150", align="left"] +{empty}[http://github.com/ngkaicong[github]] +{empty}[https://cs2113-ay1819s2-t11-3.github.io/main/team/kaicong.html[portfolio]] Role: Developer + -Responsibilities: Dev Ops + Threading +//@@author ''' -=== Benson Meier -image::yl_coder.jpg[width="150", align="left"] -{empty}[http://github.com/yl-coder[github]] [<>] +=== Jacob +image::jacobhan.png[width="150", align="left"] +{empty}[http://github.com/jacobhan[github]] +{empty}[https://cs2113-ay1819s2-t11-3.github.io/main/team/jacobhan.html[portfolio]] + Role: Developer + -Responsibilities: UI + ''' + diff --git a/docs/ContactUs.adoc b/docs/ContactUs.adoc index 5de5363abffd..769ceeee7603 100644 --- a/docs/ContactUs.adoc +++ b/docs/ContactUs.adoc @@ -2,6 +2,6 @@ :site-section: ContactUs :stylesDir: stylesheets -* *Bug reports, Suggestions* : Post in our https://github.com/se-edu/addressbook-level4/issues[issue tracker] if you noticed bugs or have suggestions on how to improve. +* *Bug reports, Suggestions* : Post in our https://github.com/cs2113-ay1819s2-t11-3/main/issues[issue tracker] if you noticed bugs or have suggestions on how to improve. * *Contributing* : We welcome pull requests. Follow the process described https://github.com/oss-generic/process[here] -* *Email us* : You can also reach us at `damith [at] comp.nus.edu.sg` +* *Email us* : You can also reach us at `kaicong@gmail.com`, `p.yushao2@gmail.com`, `frankquekch@gmail.com`, 'jacobhan1013@gmail.com' diff --git a/docs/DeveloperGuide.adoc b/docs/DeveloperGuide.adoc index 8b92d5fb7e62..344c4336b3cc 100644 --- a/docs/DeveloperGuide.adoc +++ b/docs/DeveloperGuide.adoc @@ -1,4 +1,4 @@ -= AddressBook Level 4 - Developer Guide += Budgeter - Developer Guide :site-section: DeveloperGuide :toc: :toc-title: @@ -13,9 +13,9 @@ ifdef::env-github[] :warning-caption: :warning: :experimental: endif::[] -:repoURL: https://github.com/se-edu/addressbook-level4/tree/master +:repoURL: https://github.com/cs2113-ay1819s2-t11-3/main -By: `Team SE-EDU`      Since: `Jun 2016`      Licence: `MIT` +By: `T11-3`      Since: `Jan 2019`      Licence: `MIT` == Setting up @@ -47,14 +47,14 @@ Do not disable them. If you have disabled them, go to `File` > `Settings` > `Plu . Click `OK` to accept the default settings . Open a console and run the command `gradlew processResources` (Mac/Linux: `./gradlew processResources`). It should finish with the `BUILD SUCCESSFUL` message. + This will generate all resources required by the application and tests. -. Open link:{repoURL}/src/main/java/seedu/address/ui/MainWindow.java[`MainWindow.java`] and check for any code errors +. Open link:{repoURL}/src/main/java/seedu/budgeteer/ui/MainWindow.java[`MainWindow.java`] and check for any code errors .. Due to an ongoing https://youtrack.jetbrains.com/issue/IDEA-189060[issue] with some of the newer versions of IntelliJ, code errors may be detected even if the project can be built and run successfully .. To resolve this, place your cursor over any of the code section highlighted in red. Press kbd:[ALT + ENTER], and select `Add '--add-modules=...' to module compiler options` for each error -. Repeat this for the test folder as well (e.g. check link:{repoURL}/src/test/java/seedu/address/ui/HelpWindowTest.java[`HelpWindowTest.java`] for code errors, and if so, resolve it the same way) +. Repeat this for the test folder as well (e.g. check link:{repoURL}/src/test/java/seedu/budgeteer/ui/HelpWindowTest.java[`HelpWindowTest.java`] for code errors, and if so, resolve it the same way) === Verifying the setup -. Run the `seedu.address.MainApp` and try a few commands +. Run the `seedu.budgeteer.MainApp` and try a few commands . <> to ensure they all pass. === Configurations to do before writing code @@ -74,9 +74,9 @@ Optionally, you can follow the <> docume ==== Updating documentation to match your fork -After forking the repo, the documentation will still have the SE-EDU branding and refer to the `se-edu/addressbook-level4` repo. +After forking the repo, the documentation will still have the SE-EDU branding and refer to the `se-edu/budgeteer-level4` repo. -If you plan to develop this fork as a separate product (i.e. instead of contributing to `se-edu/addressbook-level4`), you should do the following: +If you plan to develop this fork as a separate product (i.e. instead of contributing to `se-edu/budgeteer-level4`), you should do the following: . Configure the <> in link:{repoURL}/build.gradle[`build.gradle`], such as the `site-name`, to suit your own project. @@ -116,7 +116,7 @@ The *_Architecture Diagram_* given above explains the high-level design of the A [TIP] The `.pptx` files used to create diagrams in this document can be found in the link:{repoURL}/docs/diagrams/[diagrams] folder. To update a diagram, modify the diagram in the pptx file, select the objects of the diagram, and choose `Save as picture`. -`Main` has only one class called link:{repoURL}/src/main/java/seedu/address/MainApp.java[`MainApp`]. It is responsible for, +`Main` has only one class called link:{repoURL}/src/main/java/seedu/budgeteer/MainApp.java[`MainApp`]. 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 method where necessary. @@ -149,7 +149,7 @@ image::LogicClassDiagram.png[width="800"] The _Sequence Diagram_ below shows how the components interact with each other for the scenario where the user issues the command `delete 1`. .Component interactions for `delete 1` command -image::SDforDeletePerson.png[width="800"] +image::SDforDeleteEntry.png[width="800"] The sections below give more details of each component. @@ -159,11 +159,11 @@ The sections below give more details of each component. .Structure of the UI Component image::UiClassDiagram.png[width="800"] -*API* : link:{repoURL}/src/main/java/seedu/address/ui/Ui.java[`Ui.java`] +*API* : link:{repoURL}/src/main/java/seedu/budgeteer/ui/Ui.java[`Ui.java`] -The UI consists of a `MainWindow` that is made up of parts e.g.`CommandBox`, `ResultDisplay`, `PersonListPanel`, `StatusBarFooter`, `BrowserPanel` etc. All these, including the `MainWindow`, inherit from the abstract `UiPart` class. +The UI consists of a `MainWindow` that is made up of parts e.g.`CommandBox`, `ResultDisplay`, `EntryListPanel`, `StatusBarFooter`, `BrowserPanel` etc. All these, including the `MainWindow`, inherit from the abstract `UiPart` class. -The `UI` component uses 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 link:{repoURL}/src/main/java/seedu/address/ui/MainWindow.java[`MainWindow`] is specified in link:{repoURL}/src/main/resources/view/MainWindow.fxml[`MainWindow.fxml`] +The `UI` component uses 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 link:{repoURL}/src/main/java/seedu/budgeteer/ui/MainWindow.java[`MainWindow`] is specified in link:{repoURL}/src/main/resources/view/MainWindow.fxml[`MainWindow.fxml`] The `UI` component, @@ -178,9 +178,9 @@ The `UI` component, image::LogicClassDiagram.png[width="800"] *API* : -link:{repoURL}/src/main/java/seedu/address/logic/Logic.java[`Logic.java`] +link:{repoURL}/src/main/java/seedu/budgeteer/logic/Logic.java[`Logic.java`] -. `Logic` uses the `AddressBookParser` class to parse the user command. +. `Logic` uses the `EntriesBookParser` class to parse the user command. . This results in a `Command` object which is executed by the `LogicManager`. . The command execution can affect the `Model` (e.g. adding a person). . The result of the command execution is encapsulated as a `CommandResult` object which is passed back to the `Ui`. @@ -189,7 +189,7 @@ link:{repoURL}/src/main/java/seedu/address/logic/Logic.java[`Logic.java`] Given below is the Sequence Diagram for interactions within the `Logic` component for the `execute("delete 1")` API call. .Interactions Inside the Logic Component for the `delete 1` Command -image::DeletePersonSdForLogic.png[width="800"] +image::DeleteEntrySdForLogic.png[width="800"] [[Design-Model]] === Model component @@ -197,17 +197,17 @@ image::DeletePersonSdForLogic.png[width="800"] .Structure of the Model Component image::ModelClassDiagram.png[width="800"] -*API* : link:{repoURL}/src/main/java/seedu/address/model/Model.java[`Model.java`] +*API* : link:{repoURL}/src/main/java/seedu/budgeteer/model/Model.java[`Model.java`] The `Model`, * stores a `UserPref` object that represents the user's preferences. -* stores the Address Book data. -* exposes 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 the Budgeteer data. +* exposes 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 three components. [NOTE] -As a more OOP model, we can store a `Tag` list in `Address Book`, which `Person` can reference. This would allow `Address Book` to only require one `Tag` object per unique `Tag`, instead of each `Person` needing their own `Tag` object. An example of how such a model may look like is given below. + +As a more OOP model, we can store a `Tag` list in `Budgeteer`, which `Entry` can reference. This would allow `Budgeteer` to only require one `Tag` object per unique `Tag`, instead of each `Entry` needing their own `Tag` object. An example of how such a model may look like is given below. + + image:ModelClassBetterOopDiagram.png[width="800"] @@ -217,17 +217,17 @@ image:ModelClassBetterOopDiagram.png[width="800"] .Structure of the Storage Component image::StorageClassDiagram.png[width="800"] -*API* : link:{repoURL}/src/main/java/seedu/address/storage/Storage.java[`Storage.java`] +*API* : link:{repoURL}/src/main/java/seedu/budgeteer/storage/Storage.java[`Storage.java`] The `Storage` component, * can save `UserPref` objects in json format and read it back. -* can save the Address Book data in json format and read it back. +* can save the Budgeteer data in json format and read it back. [[Design-Commons]] === Common classes -Classes used by multiple components are in the `seedu.addressbook.commons` package. +Classes used by multiple components are in the `seedu.budgeteer.commons` package. == Implementation @@ -237,54 +237,54 @@ This section describes some noteworthy details on how certain features are imple === Undo/Redo feature ==== Current Implementation -The undo/redo mechanism is facilitated by `VersionedAddressBook`. -It extends `AddressBook` with an undo/redo history, stored internally as an `addressBookStateList` and `currentStatePointer`. +The undo/redo mechanism is facilitated by `VersionedEntriesBook`. +It extends `EntriesBook` with an undo/redo history, stored internally as an `budgeteerBookStateList` and `currentStatePointer`. Additionally, it implements the following operations: -* `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. +* `VersionedEntriesBook#commit()` -- Saves the current budgeteer book state in its history. +* `VersionedEntriesBook#undo()` -- Restores the previous budgeteer book state from its history. +* `VersionedEntriesBook#redo()` -- Restores a previously undone budgeteer book state from its history. -These operations are exposed in the `Model` interface as `Model#commitAddressBook()`, `Model#undoAddressBook()` and `Model#redoAddressBook()` respectively. +These operations are exposed in the `Model` interface as `Model#commitEntriesBook()`, `Model#undoEntriesBook()` and `Model#redoEntriesBook()` respectively. Given below is an example usage scenario and how the undo/redo mechanism behaves at each step. -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. +Step 1. The user launches the application for the first time. The `VersionedEntriesBook` will be initialized with the initial budgeteer book state, and the `currentStatePointer` pointing to that single budgeteer book state. image::UndoRedoStartingStateListDiagram.png[width="800"] -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. +Step 2. The user executes `delete 5` command to delete the 5th person in the budgeteer book. The `delete` command calls `Model#commitEntriesBook()`, causing the modified state of the budgeteer book after the `delete 5` command executes to be saved in the `budgeteerBookStateList`, and the `currentStatePointer` is shifted to the newly inserted budgeteer book state. image::UndoRedoNewCommand1StateListDiagram.png[width="800"] -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`. +Step 3. The user executes `add n/David ...` to add a new person. The `add` command also calls `Model#commitEntriesBook()`, causing another modified budgeteer book state to be saved into the `budgeteerBookStateList`. image::UndoRedoNewCommand2StateListDiagram.png[width="800"] [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`. +If a command fails its execution, it will not call `Model#commitEntriesBook()`, so the budgeteer book state will not be saved into the `budgeteerBookStateList`. -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. +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#undoEntriesBook()`, which will shift the `currentStatePointer` once to the left, pointing it to the previous budgeteer book state, and restores the budgeteer book to that state. image::UndoRedoExecuteUndoStateListDiagram.png[width="800"] [NOTE] -If the `currentStatePointer` is at index 0, pointing to the initial address book state, then there are no previous address book 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. +If the `currentStatePointer` is at index 0, pointing to the initial budgeteer book state, then there are no previous budgeteer book states to restore. The `undo` command uses `Model#canUndoEntriesBook()` 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: image::UndoRedoSequenceDiagram.png[width="800"] -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 `redo` command does the opposite -- it calls `Model#redoEntriesBook()`, which shifts the `currentStatePointer` once to the right, pointing to the previously undone state, and restores the budgeteer book to that state. [NOTE] -If the `currentStatePointer` is at index `addressBookStateList.size() - 1`, pointing to the latest address book state, then there are no undone address book 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. +If the `currentStatePointer` is at index `budgeteerBookStateList.size() - 1`, pointing to the latest budgeteer book state, then there are no undone budgeteer book states to restore. The `redo` command uses `Model#canRedoEntriesBook()` to check if this is the case. If so, it will return an error to the user rather than attempting to perform the redo. -Step 5. The user then decides to execute the command `list`. Commands that do not modify the address book, such as `list`, will usually not call `Model#commitAddressBook()`, `Model#undoAddressBook()` or `Model#redoAddressBook()`. Thus, the `addressBookStateList` remains unchanged. +Step 5. The user then decides to execute the command `list`. Commands that do not modify the budgeteer book, such as `list`, will usually not call `Model#commitEntriesBook()`, `Model#undoEntriesBook()` or `Model#redoEntriesBook()`. Thus, the `budgeteerBookStateList` remains unchanged. image::UndoRedoNewCommand3StateListDiagram.png[width="800"] -Step 6. The user executes `clear`, which calls `Model#commitAddressBook()`. Since the `currentStatePointer` is not pointing at the end of the `addressBookStateList`, all address book states after the `currentStatePointer` will be purged. We designed it this way because it no longer makes sense to redo the `add n/David ...` command. This is the behavior that most modern desktop applications follow. +Step 6. The user executes `clear`, which calls `Model#commitEntriesBook()`. Since the `currentStatePointer` is not pointing at the end of the `budgeteerBookStateList`, all budgeteer book states after the `currentStatePointer` will be purged. We designed it this way because it no longer makes sense to redo the `add n/David ...` command. This is the behavior that most modern desktop applications follow. image::UndoRedoNewCommand4StateListDiagram.png[width="800"] @@ -296,7 +296,7 @@ image::UndoRedoActivityDiagram.png[width="650"] ===== Aspect: How undo & redo executes -* **Alternative 1 (current choice):** Saves the entire address book. +* **Alternative 1 (current choice):** Saves the entire budgeteer book. ** Pros: Easy to implement. ** Cons: May have performance issues in terms of memory usage. * **Alternative 2:** Individual command knows how to undo/redo by itself. @@ -305,20 +305,287 @@ image::UndoRedoActivityDiagram.png[width="650"] ===== Aspect: Data structure to support the undo/redo commands -* **Alternative 1 (current choice):** Use a list to store the history of address book states. +* **Alternative 1 (current choice):** Use a list to store the history of budgeteer book states. ** Pros: Easy for new Computer Science student undergraduates to understand, who are likely to be the new incoming developers of our project. -** Cons: Logic is duplicated twice. For example, when a new command is executed, we must remember to update both `HistoryManager` and `VersionedAddressBook`. +** Cons: Logic is duplicated twice. For example, when a new command is executed, we must remember to update both `HistoryManager` and `VersionedEntriesBook`. * **Alternative 2:** Use `HistoryManager` for undo/redo ** Pros: We do not need to maintain a separate list, and just reuse what is already in the codebase. ** Cons: Requires dealing with commands that have already been undone: We must remember to skip these commands. Violates Single Responsibility Principle and Separation of Concerns as `HistoryManager` now needs to do two different things. // end::undoredo[] -// tag::dataencryption[] -=== [Proposed] Data Encryption +// tag::CommandsUISupport[] +=== Commands UI Support +The Commands UI Support feature displays the existing commands available when an alphabet is typed. + +CommandBox takes in an incomplete user command, match it with a list of command words currently supported in the application, and returns corresponding command skeleton for the user to fill in. +We have implemented dropdown menu UI for autocomplete with a list of commands prompts. +You can click on it and it will have the options automatically keyed into the command box for you. Simply type the respective data and you are ready to go. +**Current Design:** Include dropdown menu to list all autocomplete options + +**Pros:** Easier access for users at a glance, especially for first time users.+ + **Cons:** Inteferes with up and down button for command history. +**Alternative :** No Dropdown Menu +**Pros:** No need for additional UI component. +**Cons:** May not be intuitive to users who have not worked with CLI before. +// end::CommandsUISupport[] + +// tag::filter[] +=== Filter Mechanism +==== Current Implementation +This command allows users to filter through the large amount of entries and find the most relevant information they need. +It allows user to retrieve using other details such as name, date and tag. + +The below is the sequence diagram of the filter mechanism. + +image::FilterSqDg.PNG[width="790"] + +The user can use this command with only ONE of the following prefixes + +* `n/` : To search by name(s) +* `d/` : To search by date(s) +* `t/` : To search by tag(s) + +[NOTE] +The `predicate` used depends on which type of detail the user is using to find. + +The 3 predicates are as follow: + +* if search by `name` then `NameContainsKeywordsPredicate` is used +* if search by `date` then `DateContainsSpecifiedKeywordsPredicate` is used +* if search by `tag` then `TagContainsSpecifiedKeywordsPredicate` is used + +==== Design Considerations + + +It is implemented to search with one type of detail only such that only the returned results will be +shown. i.e. if a user search for an entry with date then only the date of the returned entry/s are relevant, +and other details are irrelevant to the input date during the search. + +Alternative design considered was to allow user input multiple details during each search. It may help to streamline and shorten the list of entry/s +that will be returned, but it is not useful as user may only recall some details of an entry only. + +*Example 1* + +* `Entry 1 : Name `Breakfast Joel Choo` and date `12-01-2019` +* `Entry 2` : Name `Lunch Matt Dam` and date `12-01-2019` +* `Entry 3` : Name `Dinner with Elis Yeo` and date `12-01-2019` +* User wants to find `Joel` but do not recall surname and key in `Joel` only +* User cannot recall the date and key in a random number `12-12-2019` +* There will be no results returned in this scenario as input date does not match the dates in `Entry 1` +and `Entry 2` that contains `Joel` + +*Example two* + +* `Contact 1` : Name `Lisa Jo` and tag `friends` +* User wants to find `Lisa Jo` and tag in `Lisa Jo` +* User cannot recall the date and tag in a random number `123356890` +* There will be no results returned in this scenario as input date does not match the date in the `Entry 1` although the name matches exactly. + +Hence, the user has to know at least 1 exact detail( out of name, date, tags ) that he can remember in order to have results returned. + +**Aspect:** Filtering with other details. + + +**Alternative 1 (current choice): +** Able to search with other details but only one type of data required for each search + +**Pros:** Implementation is easier when only one type of detail is used. + +**Cons:** Detail need to be exact. + + +**Alternative 2:** Able to find with vast different multiple details. + +**Pros:** More chances of get the required results as there are more options available in case user forgets the details needed. + +**Cons:** Harder to implement and increased complexity may affect efficiency. + +// end::filter[] + +// tag::display[] +=== Display Entries Mechanism +==== Current Implementation +The display command function is facilitated by `ModelManager`. + +The command is 'display' followed by the parameters such as name, date or cashflow and the order of the sorted data. + +It represents an in-memory model of the EntriesBook and is the component which manages the interactions between +the commands and the `VersionedEntriesBook`. +DisplayCommand calls `ModelManager#displayFilteredEntryList` and passes in the tag to be displayed by and whether the display +order is to be in reversed. + + This feature has one keyword `display` and takes in arguments of either tag or order of display. Keywords are +case insensitive. + +Category can be either of the following keywords: + + * `name` - To display in lexicographical order by the name attribute of the entry +* `date` - To display by the date attribute of the entry +* `cashflow` - To display by the income or expense of the entry + + Order can be either of the following: + + * `des` - To display in descending order +* `asc` - To display in ascending order + +This feature has 2 available selections as follows: + + . Single Input Parameter Mode - Input parameter can be either the tag or the order of display +* If tag specified, entries are displayed in ascending order of that tag +* If order specified, entries will be displayed by name in the specified order + + . Two Input Parameters Mode - Input parameters must contain only 1 tag and only 1 order, + and can be input in no particular order + +The input given by the user is passed to `DisplayCommandParser` to split the input separated by whitespaces to ensure +there is either only one or two arguments input by the user. These arguments are then stored in an array of strings and +the size of the array determines the mode of the command. +The strings are compared to two sets of strings containing the supported categories and orders of the function. +The string of the tag and a boolean representing whether the entrys are to be reversed will then be passed to +`ModelManager` to display the entrys. + +The following sequence diagram shows how the display operation works: + +image::DisplaySeqDg.PNG[width:800] + +==== Design Considerations +===== Aspect: Method of displaying sorted data + +* **Current Choice:** Displays the observable array list in the underlying data structure in `EntryList` +** Pros: Easy to implement, displaying of sorted entries will be permanent, user may not sort again with every following command +** Cons: User may want to have multiple data entries sorted in order beside only use one parameter. + +* **Alternative :** Sort the FilteredList of entrys obtained after filtering the underlying array list +** Pros: Does not allow the user to alter the arrangement of the underlying data, and only obtains a sorted version of +the read only data. +** Cons: Unable to sort a FilteredList as it does not support it, implementations could instead use SortedList but it +will not be able to perform the filtering function +// end::display[] + +// tag::encryption[] +=== Data Encryption + +==== Current Implementation +Currently, persons' data is stored in an XML file in plain text is not secure, hence the need to encrypt XML data. -_{Explain here how the data encryption feature will be implemented}_ +Data is encrypted using a AES-256 bits encryption. File is automatically encrypted when the Budgeter closes and decrypted when the Budgeter is started. -// end::dataencryption[] +The current implementation is just a proof of concept and will be improved upon in upcoming version. + +[NOTE] +The standard version of the JRE/JDK are under export restrictions. That also includes that some cryptographic algorithms are not allowed to be shipped in the standard version. +Replace files in library with Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files. + +==== Design Considerations + +===== Aspect: Implementation of EncryptedPassKey + +* **Current Design:** Using a password based encryption key +** Pros: Data Encryption will be easier for developers to implement this way. Encrypted Pass key will not be exposed. +** Cons: Password for the key is fixed and predetermined. + +* **Alternative :** Use the Java Cryptography Extension KeyStore +** Pros: Encryption keys will not be exposed +** Cons: Harder for developers to code + +===== Aspect: Strength of the encryption + +* **Alternative 1 (Current):** 128-bits encryption +** Pros: Using 128-bits encryption will be much faster and requires less computation resources +** Cons: Less stronger but still secure encryption +* **Alternative 2 (Prospective):** 256-bits encryption +** Pros: 256-bits provide much stronger encryption +** Cons: Requires more computation resources and might be a tad slower, albeit negligible + + +// end::encryption[] + +// tag::lock[] +=== Lock feature + +==== Current Implementation +As Budgeter contains sensitive personal financial data, apart from encrypting the XML data files, we have to keep it away from spying and stealing of data. + +As such, we could implement a password to protect the data. The password will be encrypted and store as a local copy. + +In addition the XML file will be unlocked when the password is entered to allow power users to edit the XML file. + +The password feature will be created as a class of it's own. There will be no default password for ease of use of the program. + +Users can decide whether to set a password. The current implementation is just a proof of concept and will be improved upon in upcoming version. + +The sequence diagram shows how the password command works. In the following diagram, the `password set/123` is executed. + +The following sequence diagram shows how the display operation works: + +image::lock.PNG[width="800"] + +==== Design Considerations + +===== Aspect: Password length and strength + +* **Current Design:** Password will be stored using Strings. +** Pros: Users will be able to key in alphanumeric password and developers can encoded the Strings easily. +** Cons: Harder to implement than a purely numerical password + +* **Alternative :** Password will be numerical +** Pros: Developer will be able to code it easily +** Cons: Password will be weaker, able to brute force through + +===== Aspect: Storage of password file + +* **Current Design:** Password will be stored in a encrypted file +** Pros: File will be accessible but encrypted, making it safer +** Cons: Developer will need to encrypt the file +* **Alternative :** Password will be stored in a plain text file +** Pros: Easier to store and retrieve password +** Cons: Data folder with the password need to be locked with shell file otherwise it is accessible for developers +//end:: lock[] + +// tag::report[] +=== Report feature + +==== Current Implementation +This feature allows the user to view a report (in the form of a pop-up window) +of all their financial activity within a period of time. + +The corresponding command required for this feature is `report`. + +For this feature, users have to enter 2 dates, one starting date and one ending date. +This feature is facilitated by a few key components of Bugeteer, `Logic`, `Model`, `UI` and function executions can be split into 2 phases, the `Logic phase` and the `UI phase`. + +The command takes in three optional arguments: `s/`, `e/` and `insight/` + +`s/` (Start Date): Report takes into consideration of all entries that are on or after the given start date + +`e/` (End Date): Report takes into consideration of all entries that are on or before the given end date + +Note: If both dates are given, the start date shall be before or on the same date as end date. +Otherwise the command will fail. + +`insight/` (Additional Insights): If this parameter is provided, the produced report window will provide the user with +additional information such as what their spending or income is composed of (which is based on the tags of the entries). + +image::ReportSequenceDiagram.png[width="800"] + +The start and end dates that are passed into the command are processed by the ReportCommandParser, which also generates +the predicates used to filter the list of entries based on the dates. + +To get a predicate that matches the dates between the start and end dates, the .and(predicate) method is used to make +a new predicate which is an and operation of the two predicates. + +If the `insight/` parameter is supplied, it will trigger a flag which is then used to determine if more information +should be provided to the user or not. +It provides information about the each spending and earning breakdown for the current month, presenting income and expense statistics together in one panel. + + +==== Design Considerations + +===== Aspect: Generating report + +* **Current Design:** Report will show a piechart when report command is called. +** Pros: Easier to implement and maintain. Visual much more efficient and effective to understand. +Reduce overhead during normal operations like adding, deleting and editing if we do not have to update the statistics in real time. +** Cons: This requires looping through each entry in the filtered entry list obtained from the `Model`. +To aid in the time complexity, the internal implementation of EntryList was done using hash maps instead which allowed for +constant time random access.. However, the initial filtering is close to linear time complexity which could slow down the app if many entries are inside. +Also, the list had to be created every time `summary` is called which could be slow if the command is called multiple times. + + +// end::report[] === Logging @@ -340,7 +607,204 @@ We are using `java.util.logging` package for logging. The `LogsCenter` class is Certain properties of the application can be controlled (e.g user prefs file location, logging level) through the configuration file (default: `config.json`). +// tag::stock[] +=== Stock Purchasing Power Feature +==== Current Implementation +The feature will be a command facilitated by 'StockCommand'. It will return how much of an inputted stock you can buy with your current balance with real-time market prices. +This type of feature takes advantage of external APIs, which provide the necessary financial information that's used in calculations and displayed to users. +The stock command uses the Alpha Vantage API. + +The command takes in one required argument: `n/` + +`n/` (Stock Name): The abbreviated name of the stock + +* `StockCommand#stockPrice()` - Calls the Alpha Vantage API and returns the price of the inputted stock +* `StockCommand#execute()` - Calculates how much of the stock you can purchase given your current cash flow, and displays that amount to the user as well as the price + +image::StockDiagram.png[width="800"] + +Given below is an example scenario of how the stock command mechanism behaves at each step. + +Step 1. The user launches the application, and makes earnings and cost entries to the Budgeter object. + +Step 2. The user inputs the stock command, and in the `StockCommand#stockPrice()` method, a call is made to the Alpha Vantage API and the price is returned. + +Step 3. In the `StockCommand#execute()` method, the current balance is pulled from 'ReportEntryList' and is used with the price from Step 2 to calculate purchasing power. + +Step 4. The resulting information regarding price and purchasing power is displayed to the user with a message. If the inputted stock is invalid, an error message will show. + +==== Design Considerations + +===== Aspect: How to deal with the API call + +* **Alternative 1 (current choice):** Call the Alpha Vantage API with every input. +** Pros: Easy to implement. +** Cons: Requires more wait time for each input, and may have issues regarding overall performance if the call takes longer. +* **Alternative 2:** Use threading to save the prices of each input. +** Pros: Will make the calls much faster because the price information is already saved to memory. +** Cons: There are countless stocks for users to choose from, and so the effort to implement threading would be significant. +// end::stock[] + +// tag::crypto[] +=== Cryptocurrency Purchasing Power Feature +==== Current Implementation +The feature will be a command facilitated by 'CryptoCommand'. It will return how much of an inputted cryptocurrency you can buy with your current balance with real-time market prices. +This type of feature takes advantage of external APIs, which provide the necessary financial information that's used in calculations and displayed to users. +The crypto command uses the Crypto Compare API. + +The command takes in one required argument: `n/` + +`n/` (Cryptocurrency Name): The abbreviated name of the cryptocurrency + +* `CryptoCommand#cryptoPrice()` - Calls the Crypto Compare API and returns the price of the inputted crypocurrency +* `CryptoCommand#execute()` - Calculates how much of the cryptocurrency you can purchase given your current cash flow, and displays that amount to the user as well as the price + +Given below is an example scenario of how the crypto command mechanism behaves at each step. + +Step 1. The user launches the application, and makes earnings and cost entries to the Budgeter object. + +Step 2. The user inputs the crypto command, and in the `CryptoCommand#cryptoPrice()` method, a call is made to the Crypto Compare API and the price is returned. + +Step 3. In the `CryptoCommand#execute()` method, the current balance is pulled from 'ReportEntryList' and is used with the price from Step 2 to calculate purchasing power. + +Step 4. The resulting information regarding price and purchasing power is displayed to the user with a message. If the inputted stock is invalid, an error message will show. + +==== Design Considerations + +===== Aspect: How to deal with the API call + +* **Alternative 1 (current choice):** Call the Crypto Compare API with every input. +** Pros: Easy to implement. +** Cons: Requires more wait time for each input, and may have issues regarding overall performance if the call takes longer. +* **Alternative 2:** Use threading to save the prices of each input. +** Pros: Will make the calls much faster because the price information is already saved to memory. +** Cons: There are countless cryptocurrencies for users to choose from, and so the effort to implement threading would be significant. However, this is tackled in another one of the features of Budgeter. +// end::crypto[] + +// tag::rapidcrypto[] +=== Rapid Cryptocurrency Purchasing Power Feature +==== Current Implementation +The feature will be facilitated by 'CryptoUtil' but have the commands 'bitcoin', 'ethereum', and 'litecoin'. It will return how much of these three cryptocurrencies you can buy with your current +balance with real-time market prices. However, the way that it's different from crypto command stated earlier is that it uses threading for rapid calls, and is easier to call from the command line. +This type of feature takes advantage of external APIs, which provide the necessary financial information that's used in calculations and displayed to users. +The rapid crypto command uses the Crypto Compare API. + +* `CryptoUtil#getCrypto()` - Calls the Crypto Compare API and returns the price of the inputted crypocurrency +* `CryptoUtil#checkIntervalAndUpdate()` - Using the threads created, updates them if the difference time between the calls is over 10 minutes +* `BitcoinCommand#execute()` - Either pulls the bitcoin price from the current thread or updates and returns it with a new call +* `EthereumCommand#execute()` - Either pulls the ethereum price from the current thread or updates and returns it with a new call +* `LitecoinCommand#execute()` - Either pulls the litecoin price from the current thread or updates and returns it with a new call + +Given below is an example scenario of how the crypto command mechanism behaves at each step. + +Step 1. The user launches the application, and makes earnings and cost entries to the Budgeter object. + +Step 2. The user inputs the command 'bitcoin', and in the `CryptoUtil#checkIntervalAndUpdate()` method, it checks to see if over 10 minutes has passed since the last call. +a call is made to the Crypto Compare API and the price is returned. + +Step 3. If it hasn't, then in `CryptoUtil#ruin()`, the most recent price from the thread is returned. If it has been over 10 minutes, then `CryptoUtil#getCrypto()` is +called to get the updated price of bitcoin. + +Step 4. In the `BitcoinCommand#execute()` method, the current balance is pulled from 'ReportEntryList' and is used with the price from earlier to calculate purchasing power. + +Step 5. The resulting information regarding price and purchasing power is displayed to the user with a message. If the inputted stock is invalid, an error message will show. + +[NOTE] +The example scenario is shown above with the case of the input 'bitcoin', but a similar process is also done for the inputs 'ethereum' and 'litecoin' as well. + +==== Design Considerations + +===== Aspect: How to deal with the API call + +* **Alternative 1 (current choice):** Use threading to save the prices of each input. +** Pros: Will make the calls much faster because the price information is already saved to memory. +** Cons: There are countless cryptocurrencies for users to choose from, and so the effort to implement threading is significant. +However, because the combination of bitcoin, ethereum, and litecoin have a dominance of nearly three-quarters of the total market cap of all cryptocurrencies, +it can be assumed that a user would want to invest in these three cryptocurrencies. That is why threading is used in this case but not for stocks. +* **Alternative 2:** Call the Crypto Compare API with every input. +** Pros: Easy to implement. +** Cons: Requires more wait time for each input, and may have issues regarding overall performance if the call takes longer. +// end::rapidcrypto[] + +// tag::invest[] +=== Invest with Compound Interest Feature +==== Current Implementation +The feature will be facilitated by 'InvestCommand''. It will return your hypothetical balance at a fixed interest rate over a certain number of years. + +* `InvestCommand#execute()` - Uses current balance to calculate hypothetical balance using the inputs interest rate and years, and displays it to user + +The command takes in two required argument: `interest/` and 'years/' + +`interest/` (Interest Rate): The interest rate that you want +`years/` (Years): The number of years that this interest should accumulate + +Given below is an example scenario of how the crypto command mechanism behaves at each step. + +Step 1. The user launches the application, and makes earnings and cost entries to the Budgeter object. + +Step 2. The user inputs the command 'invest' with the interest rate after 'interest' and number of years after 'years'. Only numbers are allowed as well + as maximum of one decimal point per input. + +Step 3. In the `InvestCommand#execute()` method, the current balance is pulled from 'ReportEntryList' and is used with the +inputs interest rate and years to calculate hypothetical balance. + +Step 4. The resulting information is displayed to the user with a message. If either or both the inputs are invalid, an error message will show. + +==== Design Considerations + +===== Aspect: How to deal with the API call + +* **Alternative 1 (current choice):** Allow only two inputs of interest rate and number of years +** Pros: It is a simple feature that is easy to implement that gives users insight into potential future finances. +** Cons: It's a very limited feature that doesn't show additional contributions over time. +* **Alternative 2:** Allow for detailing contributions over time in the command line +** Pros: Will be more accurate and will be a better representation of how people actually save and invest over time. +** Cons: Would be very complicated to do from the command line, so more work for the user and potentially confusing. +// end::invest[] +//@@author ngkaicong +// tag::exportexcel[] +=== Export entries from Budgeteer to Excel file. +==== Current implementation +The export into excel file mechanism is facilitated by `ModelManager` with the help of `ExcelUtil`, the utility created to handle all methods relating to Excel. It represents an in-memory model of the Budgeteer and is the component which manages the interactions between the commands, `ExcelUtil` and the `VersionedEntriesBook`. ExportExcelCommand calls `ModelManager#updateFilteredEntries` and passes in different predicates depending on the argument mode. +The List is retrieved by calling `ModelManager#getFilteredEntryList`. Meanwhile, it also called `ModelManager#getEntriesBook` to get the `ReadOnlyEntriesBook`. The SummaryByDateList is constructed after the ReadOnlyEntriesBook together with the predicate are passed into the construction of SummaryByDateList. The List is easily retrieved from SummaryByDateList by calling `SummaryByDateList#getSummaryList`. `ExcelUtil#setNameExcelFile` is called to make the Excel name based on the condition of startDate and endDate. After that, `ExcelUtil#setPathFile` is called to set the Path file, which is the location of the Excel file stored in future. +The Path file is constructed based on the name of the Excel file we retrieve above and the directory Path, it can be either optionally entered by the user or the default *User's Working Directory*. With the sufficient information, `List entries`, `List summaryList`, `file path`, `ExportExcelCommand#exportDataIntoExcelSheetWithGivenEntries` is called to start the processing of producing Excel file. + +There are 6 modes for this feature [refer to *Export the entry data from Budgeter to the Excel file* part in *User Guide*]. The mechanism that facilitates these modes can be found in the `ExportExcelCommandParser#parse`. Below is a overview of the mechanism: + +. Method `ExportExcelCommandParser#createExportExcelCommand` takes the input argument and further analyse it. +. The input given by the user is passed to `ArgumentTokeniser#tokenise` to split the input separated by prefixes. +. This returns a `ArgumentMultiMap` which contains a map with prefixes as keys and their associated input arguments as the value. +. The string associated with `d/` +.. It is then passed into `ExportExcelCommandParser#splitByWhitespace` for further processing and returns an array. This string will be split into sub-strings and each of them will be construct as a date type variable. The the size of the array exceed 2, error wil be thrown to inform invalid command format. *If the size of the string equals 1*, it is constructed as a date type variable after being passed to `ParseUtil#parseDate`, it must follow the format dd-mm-yyyy. Error will be thrown if the format is *not* correct or the date entered is *not* real. *If the size of the string equals 2*, each sub-string is constructed as a date type variable after being passed to `ParseUtil#parseDate`, and an additional check is conducted to check if the first date entered, known as Start date is smaller than or equal to the second date entered, known as End Date. +. The String associated with `dir/` +.. It is then passed into `ParseUtil#parseDirectoryString` to check if the Directory path given is existing. *If the Directory path is unreal*, an error is thrown to inform the user. +. Please take note that: +.. If the prefix `d/` is *not* entered in the input, meaning that all the entries will be included in the Excel sheet. +.. If the prefix `dir/` is *not* entered in the input, meaning that the Directory Path is default as the *User's Working Directory*. + +The `ExportExcelCommand` has four constructors which makes use of overloading to reduce code complexity. + +* One constructor has no arguments and assigns default predicate for the `FilteredList` in `ModelManager`, +`PREDICATE_SHOW_ALL_ENTRIES` which will show all items in the list and the Directory path is *User's Working Directory*. +* The second constructor takes in 2 `Date` arguments and assigns the predicate `DateIsWithinDateIntervalPredicate` which will only show items within the date interval and the Directory path is *User's Working Directory*. +* The third constructor takes in 1 `Directory Path` argument and assigns the predicate as `PREDICATE_SHOW_ALL_ENTRIES`, which will show all items in the list and the Directory path is the entered directory path. +* The fourth constructor takes in 1 `Directory Path` and 2 `Date` arguments and assigns the predicate as `DateIsWithinDateIntervalPredicate` which will only show items within the date interval and the Directory path is the entered Directory Path. + +If the Excel file with the same name and stored in same Directory exists, it will be overwritten. However, it *must* be closed before we enter the command. + + +// end::exportexcel[] + +// tag::draw_line_chart[] + +=== Draw a line chart automatically inside the Excel sheet +==== Current implementation + +This feature will automatically uses the the summary data from the `SUMMARY DATA` sheet in the Excel sheet after the command `export` is typed by user. +The feature mechanism is facilitated by `ExcelUtil`, which handles all methods related to Excel. It is the component which manages the interactions between the ExportExcelCommand with `ExcelUtil#drawChart`. + +// end::draw_line_chart[] == Documentation +//@@author We use asciidoc for writing documentation. @@ -478,14 +942,14 @@ We have two types of tests: . *GUI Tests* - These are tests involving the GUI. They include, .. _System Tests_ that test the entire App by simulating user actions on the GUI. These are in the `systemtests` package. -.. _Unit tests_ that test the individual components. These are in `seedu.address.ui` package. +.. _Unit tests_ that test the individual components. These are in `seedu.budgeteer.ui` package. . *Non-GUI Tests* - These are tests not involving the GUI. They include, .. _Unit tests_ targeting the lowest level methods/classes. + -e.g. `seedu.address.commons.StringUtilTest` +e.g. `seedu.budgeteer.commons.StringUtilTest` .. _Integration tests_ that are checking the integration of multiple code units (those code units are assumed to be working). + -e.g. `seedu.address.storage.StorageManagerTest` +e.g. `seedu.budgeteer.storage.StorageManagerTest` .. Hybrids of unit and integration tests. These test are checking multiple code units as well as how the are connected together. + -e.g. `seedu.address.logic.LogicManagerTest` +e.g. `seedu.budgeteer.logic.LogicManagerTest` === Troubleshooting Testing @@ -515,14 +979,14 @@ When a pull request has changes to asciidoc files, you can use https://www.netli Here are the steps to create a new release. -. Update the version number in link:{repoURL}/src/main/java/seedu/address/MainApp.java[`MainApp.java`]. +. Update the version number in link:{repoURL}/src/main/java/seedu/budgeteer/MainApp.java[`MainApp.java`]. . Generate a JAR file <>. . Tag the repo with the version number. e.g. `v0.1` . https://help.github.com/articles/creating-releases/[Create a new release using GitHub] and upload the JAR file you created. === Managing Dependencies -A project often depends on third-party libraries. For example, Address Book depends on the https://github.com/FasterXML/jackson[Jackson library] for JSON parsing. Managing these _dependencies_ can be automated using Gradle. For example, Gradle can download the dependencies automatically, which is better than these alternatives: +A project often depends on third-party libraries. For example, Budgeteer depends on the https://github.com/FasterXML/jackson[Jackson library] for JSON parsing. Managing these _dependencies_ can be automated using Gradle. For example, Gradle can download the dependencies automatically, which is better than these alternatives: [loweralpha] . Include those libraries in the repo (this bloats the repo size) @@ -555,41 +1019,41 @@ Do take a look at <> before attempting to modify the `Logic` compo + **** * Hints -** Just like we store each individual command word constant `COMMAND_WORD` inside `*Command.java` (e.g. link:{repoURL}/src/main/java/seedu/address/logic/commands/FindCommand.java[`FindCommand#COMMAND_WORD`], link:{repoURL}/src/main/java/seedu/address/logic/commands/DeleteCommand.java[`DeleteCommand#COMMAND_WORD`]), you need a new constant for aliases as well (e.g. `FindCommand#COMMAND_ALIAS`). -** link:{repoURL}/src/main/java/seedu/address/logic/parser/AddressBookParser.java[`AddressBookParser`] is responsible for analyzing command words. +** Just like we store each individual command word constant `COMMAND_WORD` inside `*Command.java` (e.g. link:{repoURL}/src/main/java/seedu/budgeteer/logic/commands/FindCommand.java[`FindCommand#COMMAND_WORD`], link:{repoURL}/src/main/java/seedu/budgeteer/logic/commands/DeleteCommand.java[`DeleteCommand#COMMAND_WORD`]), you need a new constant for aliases as well (e.g. `FindCommand#COMMAND_ALIAS`). +** link:{repoURL}/src/main/java/seedu/budgeteer/logic/parser/EntriesBookParser.java[`EntriesBookParser`] is responsible for analyzing command words. * Solution -** Modify the switch statement in link:{repoURL}/src/main/java/seedu/address/logic/parser/AddressBookParser.java[`AddressBookParser#parseCommand(String)`] such that both the proper command word and alias can be used to execute the same intended command. +** Modify the switch statement in link:{repoURL}/src/main/java/seedu/budgeteer/logic/parser/EntriesBookParser.java[`EntriesBookParser#parseCommand(String)`] such that both the proper command word and alias can be used to execute the same intended command. ** Add new tests for each of the aliases that you have added. ** Update the user guide to document the new aliases. -** See this https://github.com/se-edu/addressbook-level4/pull/785[PR] for the full solution. +** See this https://github.com/se-edu/budgeteer-level4/pull/785[PR] for the full solution. **** [discrete] ==== `Model` component -*Scenario:* You are in charge of `model`. One day, the `logic`-in-charge approaches you for help. He wants to implement a command such that the user is able to remove a particular tag from everyone in the address book, but the model API does not support such a functionality at the moment. Your job is to implement an API method, so that your teammate can use your API to implement his command. +*Scenario:* You are in charge of `model`. One day, the `logic`-in-charge approaches you for help. He wants to implement a command such that the user is able to remove a particular tag from everyone in the budgeteer book, but the model API does not support such a functionality at the moment. Your job is to implement an API method, so that your teammate can use your API to implement his command. [TIP] Do take a look at <> before attempting to modify the `Model` component. -. Add a `removeTag(Tag)` method. The specified tag will be removed from everyone in the address book. +. Add a `removeTag(Tag)` method. The specified tag will be removed from everyone in the budgeteer book. + **** * Hints -** The link:{repoURL}/src/main/java/seedu/address/model/Model.java[`Model`] and the link:{repoURL}/src/main/java/seedu/address/model/AddressBook.java[`AddressBook`] API need to be updated. +** The link:{repoURL}/src/main/java/seedu/budgeteer/model/Model.java[`Model`] and the link:{repoURL}/src/main/java/seedu/budgeteer/model/EntriesBook.java[`EntriesBook`] API need to be updated. ** Think about how you can use SLAP to design the method. Where should we place the main logic of deleting tags? -** Find out which of the existing API methods in link:{repoURL}/src/main/java/seedu/address/model/AddressBook.java[`AddressBook`] and link:{repoURL}/src/main/java/seedu/address/model/person/Person.java[`Person`] classes can be used to implement the tag removal logic. link:{repoURL}/src/main/java/seedu/address/model/AddressBook.java[`AddressBook`] allows you to update a person, and link:{repoURL}/src/main/java/seedu/address/model/person/Person.java[`Person`] allows you to update the tags. +** Find out which of the existing API methods in link:{repoURL}/src/main/java/seedu/budgeteer/model/EntriesBook.java[`EntriesBook`] and link:{repoURL}/src/main/java/seedu/budgeteer/model/person/Entry.java[`Entry`] classes can be used to implement the tag removal logic. link:{repoURL}/src/main/java/seedu/budgeteer/model/EntriesBook.java[`EntriesBook`] allows you to update a person, and link:{repoURL}/src/main/java/seedu/budgeteer/model/person/Entry.java[`Entry`] allows you to update the tags. * Solution -** Implement a `removeTag(Tag)` method in link:{repoURL}/src/main/java/seedu/address/model/AddressBook.java[`AddressBook`]. Loop through each person, and remove the `tag` from each person. -** Add a new API method `deleteTag(Tag)` in link:{repoURL}/src/main/java/seedu/address/model/ModelManager.java[`ModelManager`]. Your link:{repoURL}/src/main/java/seedu/address/model/ModelManager.java[`ModelManager`] should call `AddressBook#removeTag(Tag)`. +** Implement a `removeTag(Tag)` method in link:{repoURL}/src/main/java/seedu/budgeteer/model/EntriesBook.java[`EntriesBook`]. Loop through each person, and remove the `tag` from each person. +** Add a new API method `deleteTag(Tag)` in link:{repoURL}/src/main/java/seedu/budgeteer/model/ModelManager.java[`ModelManager`]. Your link:{repoURL}/src/main/java/seedu/budgeteer/model/ModelManager.java[`ModelManager`] should call `EntriesBook#removeTag(Tag)`. ** Add new tests for each of the new public methods that you have added. -** See this https://github.com/se-edu/addressbook-level4/pull/790[PR] for the full solution. +** See this https://github.com/se-edu/budgeteer-level4/pull/790[PR] for the full solution. **** [discrete] ==== `Ui` component -*Scenario:* You are in charge of `ui`. During a beta testing session, your team is observing how the users use your address book application. You realize that one of the users occasionally tries to delete non-existent tags from a contact, because the tags all look the same visually, and the user got confused. Another user made a typing mistake in his command, but did not realize he had done so because the error message wasn't prominent enough. A third user keeps scrolling down the list, because he keeps forgetting the index of the last person in the list. Your job is to implement improvements to the UI to solve all these problems. +*Scenario:* You are in charge of `ui`. During a beta testing session, your team is observing how the users use your budgeteer book application. You realize that one of the users occasionally tries to delete non-existent tags from a contact, because the tags all look the same visually, and the user got confused. Another user made a typing mistake in his command, but did not realize he had done so because the error message wasn't prominent enough. A third user keeps scrolling down the list, because he keeps forgetting the index of the last person in the list. Your job is to implement improvements to the UI to solve all these problems. [TIP] Do take a look at <> before attempting to modify the `UI` component. @@ -606,16 +1070,16 @@ image::getting-started-ui-tag-after.png[width="300"] + **** * Hints -** The tag labels are created inside link:{repoURL}/src/main/java/seedu/address/ui/PersonCard.java[the `PersonCard` constructor] (`new Label(tag.tagName)`). https://docs.oracle.com/javase/8/javafx/api/javafx/scene/control/Label.html[JavaFX's `Label` class] allows you to modify the style of each Label, such as changing its color. +** The tag labels are created inside link:{repoURL}/src/main/java/seedu/budgeteer/ui/EntryCard.java[the `EntryCard` constructor] (`new Label(tag.tagName)`). https://docs.oracle.com/javase/8/javafx/api/javafx/scene/control/Label.html[JavaFX's `Label` class] allows you to modify the style of each Label, such as changing its color. ** Use the .css attribute `-fx-background-color` to add a color. ** You may wish to modify link:{repoURL}/src/main/resources/view/DarkTheme.css[`DarkTheme.css`] to include some pre-defined colors using css, especially if you have experience with web-based css. * Solution -** You can modify the existing test methods for `PersonCard` 's to include testing the tag's color as well. -** See this https://github.com/se-edu/addressbook-level4/pull/798[PR] for the full solution. +** You can modify the existing test methods for `EntryCard` 's to include testing the tag's color as well. +** See this https://github.com/se-edu/budgeteer-level4/pull/798[PR] for the full solution. *** The PR uses the hash code of the tag names to generate a color. This is deliberately designed to ensure consistent colors each time the application runs. You may wish to expand on this design to include additional features, such as allowing users to set their own tag colors, and directly saving the colors to storage, so that tags retain their colors even if the hash code algorithm changes. **** -. Modify link:{repoURL}/src/main/java/seedu/address/commons/events/ui/NewResultAvailableEvent.java[`NewResultAvailableEvent`] such that link:{repoURL}/src/main/java/seedu/address/ui/ResultDisplay.java[`ResultDisplay`] can show a different style on error (currently it shows the same regardless of errors). +. Modify link:{repoURL}/src/main/java/seedu/budgeteer/commons/events/ui/NewResultAvailableEvent.java[`NewResultAvailableEvent`] such that link:{repoURL}/src/main/java/seedu/budgeteer/ui/ResultDisplay.java[`ResultDisplay`] can show a different style on error (currently it shows the same regardless of errors). + **Before** + @@ -627,19 +1091,19 @@ image::getting-started-ui-result-after.png[width="200"] + **** * Hints -** link:{repoURL}/src/main/java/seedu/address/commons/events/ui/NewResultAvailableEvent.java[`NewResultAvailableEvent`] is raised by link:{repoURL}/src/main/java/seedu/address/ui/CommandBox.java[`CommandBox`] which also knows whether the result is a success or failure, and is caught by link:{repoURL}/src/main/java/seedu/address/ui/ResultDisplay.java[`ResultDisplay`] which is where we want to change the style to. -** Refer to link:{repoURL}/src/main/java/seedu/address/ui/CommandBox.java[`CommandBox`] for an example on how to display an error. +** link:{repoURL}/src/main/java/seedu/budgeteer/commons/events/ui/NewResultAvailableEvent.java[`NewResultAvailableEvent`] is raised by link:{repoURL}/src/main/java/seedu/budgeteer/ui/CommandBox.java[`CommandBox`] which also knows whether the result is a success or failure, and is caught by link:{repoURL}/src/main/java/seedu/budgeteer/ui/ResultDisplay.java[`ResultDisplay`] which is where we want to change the style to. +** Refer to link:{repoURL}/src/main/java/seedu/budgeteer/ui/CommandBox.java[`CommandBox`] for an example on how to display an error. * Solution -** Modify link:{repoURL}/src/main/java/seedu/address/commons/events/ui/NewResultAvailableEvent.java[`NewResultAvailableEvent`] 's constructor so that users of the event can indicate whether an error has occurred. -** Modify link:{repoURL}/src/main/java/seedu/address/ui/ResultDisplay.java[`ResultDisplay#handleNewResultAvailableEvent(NewResultAvailableEvent)`] to react to this event appropriately. +** Modify link:{repoURL}/src/main/java/seedu/budgeteer/commons/events/ui/NewResultAvailableEvent.java[`NewResultAvailableEvent`] 's constructor so that users of the event can indicate whether an error has occurred. +** Modify link:{repoURL}/src/main/java/seedu/budgeteer/ui/ResultDisplay.java[`ResultDisplay#handleNewResultAvailableEvent(NewResultAvailableEvent)`] to react to this event appropriately. ** You can write two different kinds of tests to ensure that the functionality works: *** The unit tests for `ResultDisplay` can be modified to include verification of the color. -*** The system tests link:{repoURL}/src/test/java/systemtests/AddressBookSystemTest.java[`AddressBookSystemTest#assertCommandBoxShowsDefaultStyle() and AddressBookSystemTest#assertCommandBoxShowsErrorStyle()`] to include verification for `ResultDisplay` as well. -** See this https://github.com/se-edu/addressbook-level4/pull/799[PR] for the full solution. +*** The system tests link:{repoURL}/src/test/java/systemtests/EntriesBookSystemTest.java[`EntriesBookSystemTest#assertCommandBoxShowsDefaultStyle() and EntriesBookSystemTest#assertCommandBoxShowsErrorStyle()`] to include verification for `ResultDisplay` as well. +** See this https://github.com/se-edu/budgeteer-level4/pull/799[PR] for the full solution. *** Do read the commits one at a time if you feel overwhelmed. **** -. Modify the link:{repoURL}/src/main/java/seedu/address/ui/StatusBarFooter.java[`StatusBarFooter`] to show the total number of people in the address book. +. Modify the link:{repoURL}/src/main/java/seedu/budgeteer/ui/StatusBarFooter.java[`StatusBarFooter`] to show the total number of people in the budgeteer book. + **Before** + @@ -652,31 +1116,31 @@ image::getting-started-ui-status-after.png[width="500"] **** * Hints ** link:{repoURL}/src/main/resources/view/StatusBarFooter.fxml[`StatusBarFooter.fxml`] will need a new `StatusBar`. Be sure to set the `GridPane.columnIndex` properly for each `StatusBar` to avoid misalignment! -** link:{repoURL}/src/main/java/seedu/address/ui/StatusBarFooter.java[`StatusBarFooter`] needs to initialize the status bar on application start, and to update it accordingly whenever the address book is updated. +** link:{repoURL}/src/main/java/seedu/budgeteer/ui/StatusBarFooter.java[`StatusBarFooter`] needs to initialize the status bar on application start, and to update it accordingly whenever the budgeteer book is updated. * Solution -** Modify the constructor of link:{repoURL}/src/main/java/seedu/address/ui/StatusBarFooter.java[`StatusBarFooter`] to take in the number of persons when the application just started. -** Use link:{repoURL}/src/main/java/seedu/address/ui/StatusBarFooter.java[`StatusBarFooter#handleAddressBookChangedEvent(AddressBookChangedEvent)`] to update the number of persons whenever there are new changes to the addressbook. +** Modify the constructor of link:{repoURL}/src/main/java/seedu/budgeteer/ui/StatusBarFooter.java[`StatusBarFooter`] to take in the number of persons when the application just started. +** Use link:{repoURL}/src/main/java/seedu/budgeteer/ui/StatusBarFooter.java[`StatusBarFooter#handleEntriesBookChangedEvent(EntriesBookChangedEvent)`] to update the number of persons whenever there are new changes to the budgeteer. ** For tests, modify link:{repoURL}/src/test/java/guitests/guihandles/StatusBarFooterHandle.java[`StatusBarFooterHandle`] by adding a state-saving functionality for the total number of people status, just like what we did for save location and sync status. -** For system tests, modify link:{repoURL}/src/test/java/systemtests/AddressBookSystemTest.java[`AddressBookSystemTest`] to also verify the new total number of persons status bar. -** See this https://github.com/se-edu/addressbook-level4/pull/803[PR] for the full solution. +** For system tests, modify link:{repoURL}/src/test/java/systemtests/EntriesBookSystemTest.java[`EntriesBookSystemTest`] to also verify the new total number of persons status bar. +** See this https://github.com/se-edu/budgeteer-level4/pull/803[PR] for the full solution. **** [discrete] ==== `Storage` component -*Scenario:* You are in charge of `storage`. For your next project milestone, your team plans to implement a new feature of saving the address book to the cloud. However, the current implementation of the application constantly saves the address book after the execution of each command, which is not ideal if the user is working on limited internet connection. Your team decided that the application should instead save the changes to a temporary local backup file first, and only upload to the cloud after the user closes the application. Your job is to implement a backup API for the address book storage. +*Scenario:* You are in charge of `storage`. For your next project milestone, your team plans to implement a new feature of saving the budgeteer book to the cloud. However, the current implementation of the application constantly saves the budgeteer book after the execution of each command, which is not ideal if the user is working on limited internet connection. Your team decided that the application should instead save the changes to a temporary local backup file first, and only upload to the cloud after the user closes the application. Your job is to implement a backup API for the budgeteer book storage. [TIP] Do take a look at <> before attempting to modify the `Storage` component. -. Add a new method `backupAddressBook(ReadOnlyAddressBook)`, so that the address book can be saved in a fixed temporary location. +. Add a new method `backupEntriesBook(ReadOnlyEntriesBook)`, so that the budgeteer book can be saved in a fixed temporary location. + **** * Hint -** Add the API method in link:{repoURL}/src/main/java/seedu/address/storage/AddressBookStorage.java[`AddressBookStorage`] interface. -** Implement the logic in link:{repoURL}/src/main/java/seedu/address/storage/StorageManager.java[`StorageManager`] and link:{repoURL}/src/main/java/seedu/address/storage/JsonAddressBookStorage.java[`JsonAddressBookStorage`] class. +** Add the API method in link:{repoURL}/src/main/java/seedu/budgeteer/storage/EntriesBookStorage.java[`EntriesBookStorage`] interface. +** Implement the logic in link:{repoURL}/src/main/java/seedu/budgeteer/storage/StorageManager.java[`StorageManager`] and link:{repoURL}/src/main/java/seedu/budgeteer/storage/JsonEntriesBookStorage.java[`JsonEntriesBookStorage`] class. * Solution -** See this https://github.com/se-edu/addressbook-level4/pull/594[PR] for the full solution. +** See this https://github.com/se-edu/budgeteer-level4/pull/594[PR] for the full solution. **** [[GetStartedProgramming-RemarkCommand]] @@ -684,7 +1148,7 @@ Do take a look at <> before attempting to modify the `Storage` c By creating this command, you will get a chance to learn how to implement a feature end-to-end, touching all major components of the app. -*Scenario:* You are a software maintainer for `addressbook`, as the former developer team has moved on to new projects. The current users of your application have a list of new feature requests that they hope the software will eventually have. The most popular request is to allow adding additional comments/notes about a particular contact, by providing a flexible `remark` field for each contact, rather than relying on tags alone. After designing the specification for the `remark` command, you are convinced that this feature is worth implementing. Your job is to implement the `remark` command. +*Scenario:* You are a software maintainer for `budgeteer`, as the former developer team has moved on to new projects. The current users of your application have a list of new feature requests that they hope the software will eventually have. The most popular request is to allow adding additional comments/notes about a particular contact, by providing a flexible `remark` field for each contact, rather than relying on tags alone. After designing the specification for the `remark` command, you are convinced that this feature is worth implementing. Your job is to implement the `remark` command. ==== Description Edits the remark for a person specified in the `INDEX`. + @@ -704,13 +1168,13 @@ Let's start by teaching the application how to parse a `remark` command. We will **Main:** -. Add a `RemarkCommand` that extends link:{repoURL}/src/main/java/seedu/address/logic/commands/Command.java[`Command`]. Upon execution, it should just throw an `Exception`. -. Modify link:{repoURL}/src/main/java/seedu/address/logic/parser/AddressBookParser.java[`AddressBookParser`] to accept a `RemarkCommand`. +. Add a `RemarkCommand` that extends link:{repoURL}/src/main/java/seedu/budgeteer/logic/commands/Command.java[`Command`]. Upon execution, it should just throw an `Exception`. +. Modify link:{repoURL}/src/main/java/seedu/budgeteer/logic/parser/EntriesBookParser.java[`EntriesBookParser`] to accept a `RemarkCommand`. **Tests:** . Add `RemarkCommandTest` that tests that `execute()` throws an Exception. -. Add new test method to link:{repoURL}/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java[`AddressBookParserTest`], which tests that typing "remark" returns an instance of `RemarkCommand`. +. Add new test method to link:{repoURL}/src/test/java/seedu/budgeteer/logic/parser/EntriesBookParserTest.java[`EntriesBookParserTest`], which tests that typing "remark" returns an instance of `RemarkCommand`. ===== [Step 2] Logic: Teach the app to accept 'remark' arguments Let's teach the application to parse arguments that our `remark` command will accept. E.g. `1 r/Likes to drink coffee.` @@ -719,50 +1183,50 @@ Let's teach the application to parse arguments that our `remark` command will ac . Modify `RemarkCommand` to take in an `Index` and `String` and print those two parameters as the error message. . Add `RemarkCommandParser` that knows how to parse two arguments, one index and one with prefix 'r/'. -. Modify link:{repoURL}/src/main/java/seedu/address/logic/parser/AddressBookParser.java[`AddressBookParser`] to use the newly implemented `RemarkCommandParser`. +. Modify link:{repoURL}/src/main/java/seedu/budgeteer/logic/parser/EntriesBookParser.java[`EntriesBookParser`] to use the newly implemented `RemarkCommandParser`. **Tests:** . Modify `RemarkCommandTest` to test the `RemarkCommand#equals()` method. . Add `RemarkCommandParserTest` that tests different boundary values for `RemarkCommandParser`. -. Modify link:{repoURL}/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java[`AddressBookParserTest`] to test that the correct command is generated according to the user input. +. Modify link:{repoURL}/src/test/java/seedu/budgeteer/logic/parser/EntriesBookParserTest.java[`EntriesBookParserTest`] to test that the correct command is generated according to the user input. -===== [Step 3] Ui: Add a placeholder for remark in `PersonCard` -Let's add a placeholder on all our link:{repoURL}/src/main/java/seedu/address/ui/PersonCard.java[`PersonCard`] s to display a remark for each person later. +===== [Step 3] Ui: Add a placeholder for remark in `EntryCard` +Let's add a placeholder on all our link:{repoURL}/src/main/java/seedu/budgeteer/ui/EntryCard.java[`EntryCard`] s to display a remark for each person later. **Main:** -. Add a `Label` with any random text inside link:{repoURL}/src/main/resources/view/PersonListCard.fxml[`PersonListCard.fxml`]. -. Add FXML annotation in link:{repoURL}/src/main/java/seedu/address/ui/PersonCard.java[`PersonCard`] to tie the variable to the actual label. +. Add a `Label` with any random text inside link:{repoURL}/src/main/resources/view/EntryListCard.fxml[`EntryListCard.fxml`]. +. Add FXML annotation in link:{repoURL}/src/main/java/seedu/budgeteer/ui/EntryCard.java[`EntryCard`] to tie the variable to the actual label. **Tests:** -. Modify link:{repoURL}/src/test/java/guitests/guihandles/PersonCardHandle.java[`PersonCardHandle`] so that future tests can read the contents of the remark label. +. Modify link:{repoURL}/src/test/java/guitests/guihandles/EntryCardHandle.java[`EntryCardHandle`] so that future tests can read the contents of the remark label. ===== [Step 4] Model: Add `Remark` class -We have to properly encapsulate the remark in our link:{repoURL}/src/main/java/seedu/address/model/person/Person.java[`Person`] class. Instead of just using a `String`, let's follow the conventional class structure that the codebase already uses by adding a `Remark` class. +We have to properly encapsulate the remark in our link:{repoURL}/src/main/java/seedu/budgeteer/model/person/Entry.java[`Entry`] class. Instead of just using a `String`, let's follow the conventional class structure that the codebase already uses by adding a `Remark` class. **Main:** -. Add `Remark` to model component (you can copy from link:{repoURL}/src/main/java/seedu/address/model/person/Address.java[`Address`], remove the regex and change the names accordingly). +. Add `Remark` to model component (you can copy from link:{repoURL}/src/main/java/seedu/budgeteer/model/person/CashFlow.java[`CashFlow`], remove the regex and change the names accordingly). . Modify `RemarkCommand` to now take in a `Remark` instead of a `String`. **Tests:** . Add test for `Remark`, to test the `Remark#equals()` method. -===== [Step 5] Model: Modify `Person` to support a `Remark` field -Now we have the `Remark` class, we need to actually use it inside link:{repoURL}/src/main/java/seedu/address/model/person/Person.java[`Person`]. +===== [Step 5] Model: Modify `Entry` to support a `Remark` field +Now we have the `Remark` class, we need to actually use it inside link:{repoURL}/src/main/java/seedu/budgeteer/model/person/Entry.java[`Entry`]. **Main:** -. Add `getRemark()` in link:{repoURL}/src/main/java/seedu/address/model/person/Person.java[`Person`]. +. Add `getRemark()` in link:{repoURL}/src/main/java/seedu/budgeteer/model/person/Entry.java[`Entry`]. . You may assume that the user will not be able to use the `add` and `edit` commands to modify the remarks field (i.e. the person will be created without a remark). -. Modify link:{repoURL}/src/main/java/seedu/address/model/util/SampleDataUtil.java/[`SampleDataUtil`] to add remarks for the sample data (delete your `data/addressbook.json` so that the application will load the sample data when you launch it.) +. Modify link:{repoURL}/src/main/java/seedu/budgeteer/model/util/SampleDataUtil.java/[`SampleDataUtil`] to add remarks for the sample data (delete your `data/budgeteer.json` so that the application will load the sample data when you launch it.) -===== [Step 6] Storage: Add `Remark` field to `JsonAdaptedPerson` class -We now have `Remark` s for `Person` s, but they will be gone when we exit the application. Let's modify link:{repoURL}/src/main/java/seedu/address/storage/JsonAdaptedPerson.java[`JsonAdaptedPerson`] to include a `Remark` field so that it will be saved. +===== [Step 6] Storage: Add `Remark` field to `JsonAdaptedEntry` class +We now have `Remark` s for `Entry` s, but they will be gone when we exit the application. Let's modify link:{repoURL}/src/main/java/seedu/budgeteer/storage/JsonAdaptedEntry.java[`JsonAdaptedEntry`] to include a `Remark` field so that it will be saved. **Main:** @@ -770,26 +1234,26 @@ We now have `Remark` s for `Person` s, but they will be gone when we exit the ap **Tests:** -. Fix `invalidAndValidPersonAddressBook.json`, `typicalPersonsAddressBook.json`, `validAddressBook.json` etc., such that the JSON tests will not fail due to a missing `remark` field. +. Fix `invalidAndValidEntryEntriesBook.json`, `typicalEntrysEntriesBook.json`, `validEntriesBook.json` etc., such that the JSON tests will not fail due to a missing `remark` field. -===== [Step 6b] Test: Add withRemark() for `PersonBuilder` -Since `Person` can now have a `Remark`, we should add a helper method to link:{repoURL}/src/test/java/seedu/address/testutil/PersonBuilder.java[`PersonBuilder`], so that users are able to create remarks when building a link:{repoURL}/src/main/java/seedu/address/model/person/Person.java[`Person`]. +===== [Step 6b] Test: Add withRemark() for `EntryBuilder` +Since `Entry` can now have a `Remark`, we should add a helper method to link:{repoURL}/src/test/java/seedu/budgeteer/testutil/EntryBuilder.java[`EntryBuilder`], so that users are able to create remarks when building a link:{repoURL}/src/main/java/seedu/budgeteer/model/person/Entry.java[`Entry`]. **Tests:** -. Add a new method `withRemark()` for link:{repoURL}/src/test/java/seedu/address/testutil/PersonBuilder.java[`PersonBuilder`]. This method will create a new `Remark` for the person that it is currently building. -. Try and use the method on any sample `Person` in link:{repoURL}/src/test/java/seedu/address/testutil/TypicalPersons.java[`TypicalPersons`]. +. Add a new method `withRemark()` for link:{repoURL}/src/test/java/seedu/budgeteer/testutil/EntryBuilder.java[`EntryBuilder`]. This method will create a new `Remark` for the person that it is currently building. +. Try and use the method on any sample `Entry` in link:{repoURL}/src/test/java/seedu/budgeteer/testutil/TypicalEntrys.java[`TypicalEntrys`]. -===== [Step 7] Ui: Connect `Remark` field to `PersonCard` -Our remark label in link:{repoURL}/src/main/java/seedu/address/ui/PersonCard.java[`PersonCard`] is still a placeholder. Let's bring it to life by binding it with the actual `remark` field. +===== [Step 7] Ui: Connect `Remark` field to `EntryCard` +Our remark label in link:{repoURL}/src/main/java/seedu/budgeteer/ui/EntryCard.java[`EntryCard`] is still a placeholder. Let's bring it to life by binding it with the actual `remark` field. **Main:** -. Modify link:{repoURL}/src/main/java/seedu/address/ui/PersonCard.java[`PersonCard`]'s constructor to bind the `Remark` field to the `Person` 's remark. +. Modify link:{repoURL}/src/main/java/seedu/budgeteer/ui/EntryCard.java[`EntryCard`]'s constructor to bind the `Remark` field to the `Entry` 's remark. **Tests:** -. Modify link:{repoURL}/src/test/java/seedu/address/ui/testutil/GuiTestAssert.java[`GuiTestAssert#assertCardDisplaysPerson(...)`] so that it will compare the now-functioning remark label. +. Modify link:{repoURL}/src/test/java/seedu/budgeteer/ui/testutil/GuiTestAssert.java[`GuiTestAssert#assertCardDisplaysEntry(...)`] so that it will compare the now-functioning remark label. ===== [Step 8] Logic: Implement `RemarkCommand#execute()` logic We now have everything set up... but we still can't modify the remarks. Let's finish it up by adding in actual logic for our `remark` command. @@ -804,20 +1268,23 @@ We now have everything set up... but we still can't modify the remarks. Let's fi ==== Full Solution -See this https://github.com/se-edu/addressbook-level4/pull/599[PR] for the step-by-step solution. +See this https://github.com/se-edu/budgeteer-level4/pull/599[PR] for the step-by-step solution. [appendix] == Product Scope *Target user profile*: -* has a need to manage a significant number of contacts +* has a need to manage and keep track of his/her finances +* has a need of a tool to keep track and work towards a financial goal +* power user who would like to be rewarded by the use of the Command Line Interface (CLI) * prefer desktop apps over other types +* likes using commands to accomplish tasks quickly but prefers having a good Graphical User Interface (GUI) * can type fast * prefers typing over mouse input * is reasonably comfortable using CLI apps -*Value proposition*: manage contacts faster than a typical mouse/GUI driven app +*Value proposition*: manage and tracks finances faster than a typical mouse/GUI driven app [appendix] == User Stories @@ -829,15 +1296,40 @@ Priorities: High (must have) - `* * \*`, Medium (nice to have) - `* \*`, Low (un |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 |protect my data from hackers | have a private and secure usage of app + +|`* * *` |user |encrypt my data from hackers | my local copy is less hackable + +|`* * *` |user |add income details | + +|`* * *` |user |delete income details |remove entries that I no longer need + +|`* * *` |user |modify income details |make changes to existing entries -|`* * *` |user |delete a person |remove entries that I no longer need +|`* * *` |user |add expense details | -|`* * *` |user |find a person by name |locate details of persons without having to go through the entire list +|`* * *` |user |delete expense details |remove entries that I no longer need -|`* *` |user |hide <> by default |minimize chance of someone else seeing them by accident +|`* * *` |user |filter data acordingly by name or cashflow or date or tags | dont waste time finding one by one + +|`* * *` |user |display sorted data by descending or ascending order by name or cashflow or date or tags | get the most relevant data that matter to me most and understand the data + +|`* * *` |user |modify expense details |make changes to existing entries + +|`* * *` |user |give an income details a tag |categorize my income + +|`* * *` |user |give an expense details a tag |categorize my expenses + +|`* * *` |user |list income details based on tag |see my income belonging in a particular tag + +|`* * *` |user |list expenses details based on tag |see my expenses belonging in a particular tag + +|`* *` |user |add recurring income details |keep track of recurring income without having to key the information on a monthly basis + +|`* *` |user |add recurring expense details |keep track of recurring expense without having to key the information on a monthly basis + +|`* *` |user |generate a financial report |To understand my income and expenditure patterns and make necessary changes -|`*` |user with many persons in the address book |sort persons by name |locate a person easily |======================================================================= _{More to be added}_ @@ -845,33 +1337,49 @@ _{More to be added}_ [appendix] == Use Cases -(For all use cases below, the *System* is the `AddressBook` and the *Actor* is the `user`, unless specified otherwise) +(For all use cases below, the *System* is the `EntriesBook` and the *Actor* is the `user`, unless specified otherwise) [discrete] -=== Use case: Delete person +=== Use case: Add income details *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 enters income details via use of a command +2. Budgeteer displays income details and prompt the user for confirmation +3. User checks the displayed income details and confirms that the details are correct +4. Budgeteer creates a new income entry within itself + Use case ends. *Extensions* [none] -* 2a. The list is empty. +* 1a. User's input command is invalid ++ +[none] +** 1a1. System shows an error message + Use case ends. - -* 3a. The given index is invalid. +[none] +* 1b. User's input parameters are missing ++ +[none] +** 1b1. System shows error message and help text with the correct usage + +Use case ends. [none] -** 3a1. AddressBook shows an error message. +* 1c. The entry to be added is a duplicate of an existing entry ++ +[none] +** 1c1. System displays a warning message of the possible duplication. +** 1c2. User dismisses the warning message + Use case resumes at step 2. +[none] +* 3a. User inputs that the displayed details are wrong ++ + +Use case resumes at step 1. _{More to be added}_ @@ -879,8 +1387,9 @@ _{More to be added}_ == Non Functional Requirements . Should work on any <> as long as it has Java `9` or higher installed. -. Should be able to hold up to 1000 persons without a noticeable sluggishness in performance for typical usage. +. System should not have significant latency in carrying out tasks and commands due to the speed-focused CLI. . 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. +. Program should be usable by both power users and intermediate users alike. _{More to be added}_ @@ -890,9 +1399,6 @@ _{More to be added}_ [[mainstream-os]] Mainstream OS:: Windows, Linux, Unix, OS-X -[[private-contact-detail]] Private contact detail:: -A contact detail that is not meant to be shared with others - [appendix] == Product Survey diff --git a/docs/HelpWindow.html b/docs/HelpWindow.html new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/docs/UserGuide.adoc b/docs/UserGuide.adoc index 7e0070e12f49..8a9fc33b9870 100644 --- a/docs/UserGuide.adoc +++ b/docs/UserGuide.adoc @@ -1,4 +1,4 @@ -= AddressBook Level 4 - User Guide += Budgeter - User Guide :site-section: UserGuide :toc: :toc-title: @@ -12,29 +12,32 @@ ifdef::env-github[] :tip-caption: :bulb: :note-caption: :information_source: endif::[] -:repoURL: https://github.com/se-edu/addressbook-level4 +:repoURL: https://github.com/cs2113-ay1819s2-t11-3/main -By: `Team SE-EDU` Since: `Jun 2016` Licence: `MIT` +By: `T11-03` Since: `Jan 2019` Licence: `MIT` == Introduction -AddressBook Level 4 (AB4) is for those who *prefer to use a desktop app for managing contacts*. More importantly, AB4 is *optimized for those who prefer to work with a Command Line Interface* (CLI) while still having the benefits of a Graphical User Interface (GUI). If you can type fast, AB4 can get your contact management tasks done faster than traditional GUI apps. Interested? Jump to the <> to get started. Enjoy! +Budgeter is for those who prefer to use a *desktop app to manage their finances. +It is *optimized for those who prefer to work with a Command Line Interface* (CLI) while still +having the benefits of a Graphical User Interface (GUI). +Interested? Jump to the <> to get started. Enjoy! == Quick Start . Ensure you have Java version `9` or later installed in your Computer. -. Download the latest `addressbook.jar` link:{repoURL}/releases[here]. -. Copy the file to the folder you want to use as the home folder for your Address Book. +. Download the latest `budgeteer.jar` link:{repoURL}/releases[here]. +. Copy the file to the folder you want to use as the home folder for your Entries Book. . Double-click the file to start the app. The GUI should appear in a few seconds. + -image::Ui.png[width="790"] +image::ui2.png[width="790"] + . Type the command in the command box and press kbd:[Enter] to execute it. + e.g. typing *`help`* and pressing kbd:[Enter] will open the help window. . Some example commands you can try: -* *`list`* : lists all contacts -* **`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. +* *`list`* : lists all entries +* **`add`**`n/John Doe's meal d/20-02-2019 c/+100 t/John street` : adds a entry named `John Doe` to the Entries Book. * **`delete`**`3` : deletes the 3rd contact shown in the current list * *`exit`* : exits the app @@ -49,54 +52,65 @@ e.g. typing *`help`* and pressing kbd:[Enter] will open the help window. * 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`. * 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`. * Items with `…`​ after them can be used multiple times including zero times e.g. `[t/TAG]...` can be used as `{nbsp}` (i.e. 0 times), `t/friend`, `t/friend t/family` etc. -* 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. +* Parameters can be in any order e.g. if the command specifies `n/NAME p/DATE`, `p/DATE n/NAME` is also acceptable. ==== === Viewing help : `help` Format: `help` -=== Adding a person: `add` -Adds a person to the address book + -Format: `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]...` +=== Adding an entry: `add` +Adds an entry to the entries book + +Format: `add n/NAME d/DATE c/CASHFLOW [t/TAG]...` + +CASHFLOW represents the input/output of the financial activity. It can be either a output(expense) or a input(income). +To distinguish between an income and an expense, the user will need to enter a plus "+" or minus "-" sign before the money amount respectively. + +**** +* Typical format for a CASHFLOW: +** Typical example of *income*: add n/Salary d/20-2-2019 m/*+50.00* +** Typical example of *expense*: add n/BusFare d/20-2-2019 m/*-4.50* +**** [TIP] -A person can have any number of tags (including 0) + +An entry 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` +* `add n/DinnerWithKenneth d/20-02-2019 c/-5` +* `add n/IncomeFromWork c/+100 d/01-01-2019 t/Work` -=== Listing all persons : `list` +=== Listing all entries : `list` + +Shows a list of all entries in the entries book. + -Shows a list of all persons in the address book. + Format: `list` -=== Editing a person : `edit` +=== Editing an entry : `edit` -Edits an existing person in the address book. + -Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]...` +Edits an existing entry in the entries book. + +Format: `edit INDEX [n/NAME] [d/DATE] [c/CASHFLOW] [t/TAG]...` **** -* 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, ... +* Edits the entry at the specified `INDEX`. The index refers to the index number shown in the displayed entry 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. +* When editing tags, the existing tags of the entry will be removed i.e adding of tags is not cumulative. +* You can remove all the entry's tags by typing `t/` without specifying any tags after it. **** 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 1 d/12-12-2019 c/+100.0 + +Edits the date and cashflow of the 1st entry to be `12-12-2019` 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. +Edits the name of the 2nd entry to be `Betsy Crower` and clears all existing tags. -=== Locating persons by name: `find` +=== Locating entries by name: `find` -Finds persons whose names contain any of the given keywords. + +Finds entries whose names contain any of the given keywords. + Format: `find KEYWORD [MORE_KEYWORDS]` **** @@ -104,7 +118,7 @@ Format: `find KEYWORD [MORE_KEYWORDS]` * 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` +* Entries matching at least one keyword will be returned (i.e. `OR` search). e.g. `Hans Bo` will return `Hans Gruber`, `Bo Yang` **** Examples: @@ -112,16 +126,16 @@ Examples: * `find John` + Returns `john` and `John Doe` * `find Betsy Tim John` + -Returns any person having names `Betsy`, `Tim`, or `John` +Returns any entry having names `Betsy`, `Tim`, or `John` -=== Deleting a person : `delete` +=== Deleting an entry : `delete` -Deletes the specified person from the address book. + +Deletes the specified entry from the entries book. + Format: `delete INDEX` **** -* Deletes the person at the specified `INDEX`. -* The index refers to the index number shown in the displayed person list. +* Deletes the entry at the specified `INDEX`. +* The index refers to the index number shown in the displayed entry list. * The index *must be a positive integer* 1, 2, 3, ... **** @@ -129,19 +143,19 @@ Examples: * `list` + `delete 2` + -Deletes the 2nd person in the address book. +Deletes the 2nd entry in the entries book. * `find Betsy` + `delete 1` + -Deletes the 1st person in the results of the `find` command. +Deletes the 1st entry in the results of the `find` command. -=== Selecting a person : `select` +=== Selecting an entry : `select` -Selects the person identified by the index number used in the displayed person list. + +Selects the entry identified by the index number used in the displayed entry list. + Format: `select INDEX` **** -* Selects the person and loads the Google search page the person at the specified `INDEX`. -* The index refers to the index number shown in the displayed person list. +* Selects the entry and loads the Google search page the entry at the specified `INDEX`. +* The index refers to the index number shown in the displayed entry list. * The index *must be a positive integer* `1, 2, 3, ...` **** @@ -149,10 +163,311 @@ Examples: * `list` + `select 2` + -Selects the 2nd person in the address book. +Selects the 2nd entry in the entries book. * `find Betsy` + `select 1` + -Selects the 1st person in the results of the `find` command. +Selects the 1st entry in the results of the `find` command. + +// tag::filter[] +=== Filtering entrys by names or cashflows or dates or tags: `filter` + +Finds entrys who contain any of the given keywords. + +Format: `filter n/[MORE_KEYWORDS] or d/[MORE_KEYWORDS] or c/[MORE_KEYWORDS] or t/[MORE_KEYWORDS]` + +[NOTE] +Filtering using one type of details each time. + +E.g. If `filter d/12-01-2019` then `n/` and `t/` should not be included since +there is only one data information is used to filter accordingly + +**** +* The search for name is case insensitive. e.g `Income from John` will match `income from John` +* The search for cashflow is -/+ sensitive. e.g `-100` will not match `+100` +* The search for tag is case sensitive. e.g `waste` will match `waste` but not `Waste` +* The order of the keywords for name does not matter. e.g. `Alex Jo` will match `Jo Alex` +* The order of the keywords when searching a few tags does not matter. e.g. `[friends] [colleagues]` will match `[colleagues] [friends]` +* Only full words will be matched e.g. `friend` will not match `friends` +* Name matching at least one keyword will be returned e.g. `Ming Jun` will return `Ming Ho`, `Jun Xang` +**** + +Examples: + +* `filter n/Food with Alex` + +Returns `food with alex` and `Food with Alex John ` +* `filter n/Income from AIA` + +Returns any entry having names `AIA` or `Income` +* `filter d/12-01-2019` + +Returns any entry having date `12-01-2019` exactly +* `filter d/12-01-2019 12-02-2019` + +Returns any entry having dates `12-01-2019` or `12-02-2019` exactly +* `filter c/+100` + +Returns any entry having cashflow `+100` exactly +* `filter t/[friends]` + +Returns any entry having tag `[friends]` exactly +* `filter t/[family] [colleagues]` + +Returns any entry having tags `[family]` or `[colleagues]` exactly +//end::filter[] + +// tag::display[] +=== Displaying sorted entries : `display` + +Sorts the list of entries in the entry book by a category. +There are 3 categories to sort by `name`, `date`, `cashflow` and +entrys can be sorted in either ascending order `asc` or descending order `des`. + +Format: `sort [TAG] [ORDER]` + +**** +* Only the abovementioned keywords for category and order are supported. +* Keyword matching is case insensitive, e.g `sort Name Des` will work the same as `sort name desc`. +* Either one or both of the optionals fields are to be provided. +* Order of the input fields is not significant, e.g. `sort name asc` will work the same as `sort +asc name`. +* If order is not specified, default sort order is ascending. +* If category is not specified, default sort category is by name. +**** + +Examples: + +* `display date` - Sorts entries by date in ascending order. +* `display desc` - Sorts entries by name in descending order. +* `display cashflow des` - Sorts entries by cashflow in descending order. +// end::display[] + +// tag::report[] +=== Generates Report : `report` + +Shows a visual pie chart listing specified by the user when user type the command report. + +A `report` is an item that contains information on the *date or month that is represented*, the *total expense calculated*, the *total income calculated* and +the *net cash flow calculated.* + +**** +* Note that there are specific formats required for the dates and months entered. +Capital letters of REPORT is not allowed, instead report is used. +* For the commands *"report"* and *"summary category"*, START_DATE/END_DATE must be in the format of +`dd-mm-yyyy` where `dd` represents day, `mm` represents month, `yyyy` represents year. +* For command *"report"*, START_MONTH/END_MONTH must in the format of `mmm-yyyy`, `mmm` represents the month with its three letter representations, and +`yyyy` represents the year in its numerical form. + +**** + +The screenshots below are examples of what you can see once the command has been accepted. The commands entered have been left +in for visualisation purposes. These screenshots are taken in *fullscreen mode* at 1080p resolution. + +*Screenshot of app when `report` is run* + +image::ReportD.PNG[width="790"] + +*Screenshot of app when `report s/12-12-2018 e/today` is run* + +image::Report2.PNG[width="790"] + +// end::report[] + +// tag::report_insight[] +=== Report Insight : `report insight/` +Further to report feature, users want to know more and understand their spending pattern so as to +improve on what they can consume or earn. +Hence, we developed this to aid in this problem. + +Similar to report, this will show a breakdown of total expenses and income into categories and displays these information in a pie chart. + +There is 2 format for this command: + +First, Format: `report insight/` where it will show the piechart in terms of the current entries available in the Budgeter. +Second, Format:`report insight/ d/START_DATE END_DATE`where it will show the piechart in terms of the start and end dates input into the the Budgeter. + +**** +* START_DATE/END_DATE follow the same configurations as date parameters required when adding records. It is in the form of +*dd-mm-yyyy* where *dd* represents day, *mm* represents month and *yyyy* represents the year. *dd* and *mm* both require 1 to 2 digits while +*yyyy* requires exactly 4 digits. +* START_DATE and END_DATE can be 'today'. +**** + +Once the command has been executed, a window will appear showing a pie chart containing data that is relevant in the range. + +At the same time, currently selected entries will be unselected to reduce confusion for the user. If there are many categories shown and +the box is not large enough, you can use the scroll bar at the side of each legend to view the other categories which are not in view. + + +[NOTE] +Due to label constraints, some labels may not be displaying correctly if they are overlapping with other labels. This happens when the pie slice +is too small. To improve readability, we have decided to hide some labels in such scenarios. Also, when the label is too long, since the pie charts +need to fit the labels, the pie chart may become small as a result. To prevent such situations, please keep your labels short. This will be improved in +later versions of the product to remove the labels completely and use a mouse over input instead. + +Examples: + +* `report insight` + +Below are some screenshots of what you can see when the command has been accepted. The commands entered have been left +in for visualisation purposes. These screenshots are taken in *fullscreen mode* at 1080p resolution. + +image::RI1.PNG[width="790"] +*Screenshot of app displaying income breakdown when `report insight` is run* + +image::RI2.PNG[width="790"] +*Screenshot of app displaying expense breakdown when `report insight` is run* + +* `report insight d/11-11-2018 12-12-2019` + +Below are some screenshots of what you can see when the command has been accepted. The commands entered have been left +in for visualisation purposes. These screenshots are taken in *fullscreen mode* at 1080p resolution. + +image::RI3.PNG[width="790"] +*Screenshot of app displaying income breakdown when `report insight d/11-11-2018 12-12-2019` is run* + +image::RI4.PNG[width="790"] +*Screenshot of app displaying expense breakdown wgithen `report insight d/11-11-2018 12-12-2019` is run* + +// end::report_insight[] + +// tag::bitcoin[] +=== Returns purchasing power regarding bitcoin: `bitcoin` + +Returns how much bitcoin you can buy with your current balance with real-time market prices. + +Format: `bitcoin` + +[NOTE] +Calling 'bitcoin' without any entries will just return 0 as you don't have a balance yet. However, it will still return +the current price of bitcoin. + +Examples: + +* `bitcoin` + +Returns your bitcoin purchasing power, as well as the current price of bitcoin in SGD. + +// end::bitcoin[] + +// tag::ethereum[] +=== Returns purchasing power regarding ethereum: `ethereum` + +Returns how much ethereum you can buy with your current balance with real-time market prices. + +Format: `ethereum` + +[NOTE] +Calling 'ethereum' without any entries will just return 0 as you don't have a balance yet. However, it will still return +the current price of ethereum. + +Examples: + +* `ethereum` + +Returns your ethereum purchasing power, as well as the current price of ethereum in SGD. + +// end::ethereum[] + +// tag::litecoin[] +=== Returns purchasing power regarding litecoin: `litecoin` + +Returns how much litecoin you can buy with your current balance with real-time market prices. + +Format: `litecoin` + +[NOTE] +Calling 'litecoin' without any entries will just return 0 as you don't have a balance yet. However, it will still return +the current price of litecoin. + +Examples: + +* `litecoin` + +Returns your litecoin purchasing power, as well as the current price of litecoin in SGD. + +// end::litecoin[] + +// tag::crypto[] +=== Returns purchasing power regarding an inputted crypto: `crypto` + +Returns how much cryptocurrency you can buy with your current balance with real-time market prices. + +Format: `crypto n/NAME" + +[TIP] +Lower and upper case do not matter when inputting cryptocurrency names. + +[NOTE] +Calling 'cryptocurrency' with an invalid cryptocurrency name will return "Sorry, your input is not a valid cryptocurrency. Please try again." + +Examples: + +* `crypto n/BTC` + +Returns your purchasing power of the cryptocurrency Bitcoin, as well as the current price of the cryptocurrency. + + +* `crypto n/xrp` + +Returns your purchasing power of the cryptocurrency Ripple, as well as the current price of the cryptocurrency. + + +* `crypto n/asdfasdf` + +Returns "Sorry, your input is not a valid cryptocurrency. Please try again." as this is not a valid cryptocurrency name + +// end::crypto[] + +// tag::stock[] +=== Returns purchasing power regarding an inputted stock: `stock` + +Returns how much stock you can buy with your current balance with real-time market prices. + +Format: `stock n/NAME" + +[TIP] +Lower and upper case do not matter when inputting the stock names. + +[NOTE] +Calling 'stock' with an invalid stock name will return "Sorry, your input is not a valid stock. Please try again." + +Examples: + +* `stock n/MSFT` + +Returns your purchasing power of the stock for Microsoft, as well as the current price of stock. + + +* `stock n/nflx` + +Returns your purchasing power of the stock for Netflix, as well as the current price of stock. + + +* `stock n/asdfasdf` + +Returns "Sorry, your input is not a valid stock. Please try again." as this is not a valid stock name + +// end::stock[] + +// tag::invest[] +=== Returns hypothetical balance over time based on compound interest: `invest` + +Returns how much you would have at a fixed interest rate over a certain number of years. + +Format: `invest interest/INTEREST RATE years/YEARS" + +[NOTE] +Enter the interest rate in percentage. For example, 5.5% would be inputted as 5.5. + +[NOTE] +Only numbers and at most one decimal point is allowed for each numerical input. Anything else will error. + +Examples: + +* `invest interest/5.5 years/20` + +Returns your current balance and balance after 20 years with an interest rate of 5.5%. + + +* `invest interest/3 years/45.6` + +Returns your current balance and balance after 45.6 years with an interest rate of 3%. +// end::invest[] + +// tag::lock[] +=== Lock : `lock` + +Set a password for Budgeteer to protect data entry, privacy and unwanted tampering. +No password required to access the program when using for the first time. + +Format: `lock` + +The current version of setting the password is instant so after locking the Budgeteer, +will see that it is locked and to unlock, simply follow the given examples. +The application will hide all data entries when locked and encrypt the password accordingly. + +Examples: + +* `lock set/yourpassword` + +Password will be set as yourpassword. + +* `yourpassword` + +Application will be unlocked. + +[NOTE] +==== +* Currently, there are no password recovery mechanism in place. +* If users forget their password, please delete the password.txt file in the data folder to remove the password. +* The password.txt is encrypted, hence, no one can see the exact password. +* Default destination file is at the data folder. +==== +// end::lock[] + === Listing entered commands : `history` @@ -167,12 +482,12 @@ Pressing the kbd:[↑] and kbd:[↓] arrows will display the previous and // tag::undoredo[] === Undoing previous command : `undo` -Restores the address book to the state before the previous _undoable_ command was executed. + +Restores the entries book to the state before the previous _undoable_ command was executed. + Format: `undo` [NOTE] ==== -Undoable commands: those commands that modify the address book's content (`add`, `delete`, `edit` and `clear`). +Undoable commands: those commands that modify the entries book's content (`add`, `delete`, `edit` and `clear`). ==== Examples: @@ -216,7 +531,7 @@ The `redo` command fails as there are no `undo` commands executed previously. === Clearing all entries : `clear` -Clears all entries from the address book. + +Clears all entries from the entries book. + Format: `clear` === Exiting the program : `exit` @@ -226,29 +541,93 @@ Format: `exit` === Saving the data -Address book data are saved in the hard disk automatically after any command that changes the data. + +Entries book data are saved in the hard disk automatically after any command that changes the data. + There is no need to save manually. -// tag::dataencryption[] -=== Encrypting data files `[coming in v2.0]` +//@@author ngkaicong + +// tag::exportexcel[] +=== Export the entry data from Budgeter to the Excel file: `export` + +Exports the entries into an Excel file. + + +There are 6 modes, default mode, single argument mode and dual argument mode (for Date) and single argument mode (Directory Path). + + +Format: + + +**** +* *Default mode* `export` will list down all entries in Budgeter and exports all of them to an Excel file and store the file in the default *Working Directory*, it will *detect automatically user's Working Directory*. + +* *Single argument Date mode* `export d/DATE` will list down all entries with the specified date and exports all shown entries to an Excel file and store the file in the default *Working Directory*, it will *detect automatically user's Working Directory*. + +* *Dual argument Date mode* `export d/START_DATE END_DATE` will list down all entries with the date that fall on either dates or between both dates and exports all shown entries to an Excel file and store the file in the default *Working Directory*, it will *detect automatically user's Working Directory*. + +* *Single argument Directory Path mode* `export dir/DIRECTORY_PATH` will list down all entries in Savee and exports all of them to an Excel file and store the file in the chosen Directory Path. + +* *Single argument Date mode + Single argument Directory path mode* `export d/DATE dir/DIRECTORY_PATH` will list down all entries with the specified date and exports all shown entries to an Excel file and store the file in the chosen Directory Path. + +* *Dual argument Date mode + Single argument Directory path mode* `export d/START_DATE END_DATE dir/DIRECTORY_PATH` will list down all entries with the date that fall on either dates or between both dates and exports all shown entries to an Excel file and store the file in the chosen Directory Path. ++ +**** + +If the command is in *Dual argument Date mode*, START_DATE (the first `Date`) should be earlier than or equal to the END_DATE (the second `Date`). + +Date should follow the same configurations as date parameters required when adding entries. It is in the form of *dd-mm-yyyy* where *dd* represents day, *mm* represents month and *yyyy* represents the year. *dd* and *mm* both require 1 to 2 digits while *yyyy* requires exactly 4 digits. + +The Excel file name will be named based on the command, relating to Date: + + +* *Default mode*: The Excel file will be named `ENTRIES_ALL.xlsx` +* *Single argument Date mode*: The Excel file will be named `ENTRIES_dd-mm-yyyy.xlsx` +* *Dual argument Date*: The Excel file will be named `ENTRIES_dd-mm-yyyy_dd-mm-yyyy.xlsx` + +If the Excel file with the same name and stored in same Directory exists, it will be overwritten. However, it *must* be closed before we enter the command. + +After you enter the `export` command, you should *wait for few seconds* for the Excel file to be written. + +Please note that `undo` and `redo` command can only affect Budgeter but the *not* the Excel file created, meaning that when you enter `undo` command after you enter the `export` command, the Budgeter will inform the user that *No more command to undo*, the entries remain the same and the Excel file created will *not* be deleted. + +Examples: + +* `export` +* `export d/31-3-1999` +* `export dir/C:\` +* `export d/31-3-1999 31-03-2019` +* `export d/31-3-1999 dir/C:\` +* `export d/31-3-1999 31-3-2019 dir/C:\` + +// end::exportexcel[] + +// tag::draw_line_chart[] + +=== Creates line chart automatically inside the Excel sheet : `Requires no command` + +Automatically takes the summary data from the *SUMMARY DATA* tab in the Excel sheet after the command `export` is called and creates an line chart. +The screenshot below, in the *SUMMARY DATA* tab, shows the line chart. + +image::linechart.png[width="500"] + +* On the top left of the chart shows the legend with 3 lines, namely Income, Expense, and Nett. +** The blue line shows the Income based on Date. +** The orange line shows the Expense based on Date +** The grey line shows the Nett (total of income and expense) based on Date. + +// end::draw_line_chart[] -_{explain how the user can enable/disable data encryption}_ -// end::dataencryption[] +//@@author == FAQ *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 Address Book 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 Entries Book folder. == Command Summary -* *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` +* *Add* `add n/NAME d/DATE c/CASHFLOW [t/TAG]...` + +e.g. `add n/Lunch with James Ho d/12-02-2019 c/+100.00 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` +* *Edit* : `edit INDEX [n/NAME] [d/DATE] [c/CASHFLOW] [t/TAG]...` + +e.g. `edit 2 n/James Lee c/+12` * *Find* : `find KEYWORD [MORE_KEYWORDS]` + e.g. `find James Jake` * *List* : `list` @@ -256,5 +635,21 @@ e.g. `find James Jake` * *Select* : `select INDEX` + e.g.`select 2` * *History* : `history` +* *Bitcoin* : `bitcoin` +* *Ethereum* : `ethereum` +* *Litecoin* : `litecoin` +* *Stock* `stock n/NAME` + +e.g. `stock n/MSFT` +* *Crypto* `crypto n/NAME` + +e.g. `crypto n/XRP` +* *Invest* `invest interest/INTEREST RATE years/YEARS` + +e.g. `invest interest/5.5 years/20` * *Undo* : `undo` * *Redo* : `redo` +* *Report* : `report` +* *Report insight* : `Report insight` +* *Filter* : `filter` +* *Display* : `display name des` +* *Lock* : `lock set/123` +* *Unlock* : `123` +* *Export to Excel* : `export` diff --git a/docs/UsingGradle.adoc b/docs/UsingGradle.adoc index d1be2f3b7c3a..3e46435816c1 100644 --- a/docs/UsingGradle.adoc +++ b/docs/UsingGradle.adoc @@ -83,9 +83,9 @@ The set of code style rules implemented can be found in `config/checkstyle/check * **`allTests`** + Runs all tests. * **`guiTests`** + -Runs all tests in the `seedu.address.ui` and `systemtests` package +Runs all tests in the `seedu.budgeteer.ui` and `systemtests` package * **`nonGuiTests`** + -Runs all non-GUI tests in the `seedu.address` +Runs all non-GUI tests in the `seedu.budgeteer` package * **`headless`** + Sets the test mode as _headless_. The mode is effective for that Gradle run only so it should be combined with other test tasks. diff --git a/docs/diagrams/ModelComponentClassBetterOopDiagram.pptx b/docs/diagrams/ModelComponentClassBetterOopDiagram.pptx index a0b23659eb29..484762ebb0f4 100644 Binary files a/docs/diagrams/ModelComponentClassBetterOopDiagram.pptx and b/docs/diagrams/ModelComponentClassBetterOopDiagram.pptx differ diff --git a/docs/diagrams/ModelComponentClassDiagram.pptx b/docs/diagrams/ModelComponentClassDiagram.pptx index dc0e4ac5ea66..cec70b4ed9f5 100644 Binary files a/docs/diagrams/ModelComponentClassDiagram.pptx and b/docs/diagrams/ModelComponentClassDiagram.pptx differ diff --git a/docs/images/1.PNG b/docs/images/1.PNG new file mode 100644 index 000000000000..0b94f51bb7f4 Binary files /dev/null and b/docs/images/1.PNG differ diff --git a/docs/images/DeleteEntrySdForLogic.png b/docs/images/DeleteEntrySdForLogic.png new file mode 100644 index 000000000000..19144efad354 Binary files /dev/null and b/docs/images/DeleteEntrySdForLogic.png differ diff --git a/docs/images/DeletePersonSdForLogic.png b/docs/images/DeletePersonSdForLogic.png deleted file mode 100644 index 0462b9b7be6e..000000000000 Binary files a/docs/images/DeletePersonSdForLogic.png and /dev/null differ diff --git a/docs/images/DisplaySeqDg.PNG b/docs/images/DisplaySeqDg.PNG new file mode 100644 index 000000000000..0ac3a767231c Binary files /dev/null and b/docs/images/DisplaySeqDg.PNG differ diff --git a/docs/images/FilterSqDg.PNG b/docs/images/FilterSqDg.PNG new file mode 100644 index 000000000000..658a0f5c712d Binary files /dev/null and b/docs/images/FilterSqDg.PNG differ diff --git a/docs/images/ModelClassBetterOopDiagram.png b/docs/images/ModelClassBetterOopDiagram.png index b7df3a1c02b4..4ab051e09ed3 100644 Binary files a/docs/images/ModelClassBetterOopDiagram.png and b/docs/images/ModelClassBetterOopDiagram.png differ diff --git a/docs/images/ModelClassDiagram.png b/docs/images/ModelClassDiagram.png index 4961edd74e76..9c26c05ccc0f 100644 Binary files a/docs/images/ModelClassDiagram.png and b/docs/images/ModelClassDiagram.png differ diff --git a/docs/images/RI1.PNG b/docs/images/RI1.PNG new file mode 100644 index 000000000000..cd58a8c07835 Binary files /dev/null and b/docs/images/RI1.PNG differ diff --git a/docs/images/RI2.PNG b/docs/images/RI2.PNG new file mode 100644 index 000000000000..31d66832916e Binary files /dev/null and b/docs/images/RI2.PNG differ diff --git a/docs/images/RI3.PNG b/docs/images/RI3.PNG new file mode 100644 index 000000000000..8fdf6f8b0a08 Binary files /dev/null and b/docs/images/RI3.PNG differ diff --git a/docs/images/RI4.PNG b/docs/images/RI4.PNG new file mode 100644 index 000000000000..880de10ec942 Binary files /dev/null and b/docs/images/RI4.PNG differ diff --git a/docs/images/Report.PNG b/docs/images/Report.PNG new file mode 100644 index 000000000000..69237a3dedad Binary files /dev/null and b/docs/images/Report.PNG differ diff --git a/docs/images/Report2.PNG b/docs/images/Report2.PNG new file mode 100644 index 000000000000..27e056a67691 Binary files /dev/null and b/docs/images/Report2.PNG differ diff --git a/docs/images/ReportD.PNG b/docs/images/ReportD.PNG new file mode 100644 index 000000000000..367031cdda1f Binary files /dev/null and b/docs/images/ReportD.PNG differ diff --git a/docs/images/ReportSequenceDiagram.png b/docs/images/ReportSequenceDiagram.png new file mode 100644 index 000000000000..0d4dc6701841 Binary files /dev/null and b/docs/images/ReportSequenceDiagram.png differ diff --git a/docs/images/SDforDeletePerson.png b/docs/images/SDforDeleteEntry.png similarity index 100% rename from docs/images/SDforDeletePerson.png rename to docs/images/SDforDeleteEntry.png diff --git a/docs/images/StockDiagram.png b/docs/images/StockDiagram.png new file mode 100644 index 000000000000..d46fc2d10655 Binary files /dev/null and b/docs/images/StockDiagram.png differ diff --git a/docs/images/Ui.png b/docs/images/Ui.png index 5ec9c527b49c..d817f8fabf91 100644 Binary files a/docs/images/Ui.png and b/docs/images/Ui.png differ diff --git a/docs/images/damithc.jpg b/docs/images/damithc.jpg deleted file mode 100644 index 127543883893..000000000000 Binary files a/docs/images/damithc.jpg and /dev/null differ diff --git a/docs/images/frankquekch.png b/docs/images/frankquekch.png new file mode 100644 index 000000000000..2bcf8615a3aa Binary files /dev/null and b/docs/images/frankquekch.png differ diff --git a/docs/images/jacobhan.png b/docs/images/jacobhan.png new file mode 100644 index 000000000000..8f515f5d239a Binary files /dev/null and b/docs/images/jacobhan.png differ diff --git a/docs/images/lejolly.jpg b/docs/images/lejolly.jpg deleted file mode 100644 index 2d1d94e0cf5d..000000000000 Binary files a/docs/images/lejolly.jpg and /dev/null differ diff --git a/docs/images/linechart.png b/docs/images/linechart.png new file mode 100644 index 000000000000..faa74f5a740a Binary files /dev/null and b/docs/images/linechart.png differ diff --git a/docs/images/linechart2.png b/docs/images/linechart2.png new file mode 100644 index 000000000000..1b7e0979fbba Binary files /dev/null and b/docs/images/linechart2.png differ diff --git a/docs/images/lock.PNG b/docs/images/lock.PNG new file mode 100644 index 000000000000..f7922099bb96 Binary files /dev/null and b/docs/images/lock.PNG differ diff --git a/docs/images/m133225.jpg b/docs/images/m133225.jpg deleted file mode 100644 index fd14fb94593a..000000000000 Binary files a/docs/images/m133225.jpg and /dev/null differ diff --git a/docs/images/ngkaicong.png b/docs/images/ngkaicong.png new file mode 100644 index 000000000000..a979ced1599e Binary files /dev/null and b/docs/images/ngkaicong.png differ diff --git a/docs/images/yijinl.jpg b/docs/images/yijinl.jpg deleted file mode 100644 index adbf62ad9406..000000000000 Binary files a/docs/images/yijinl.jpg and /dev/null differ diff --git a/docs/images/yl_coder.jpg b/docs/images/yl_coder.jpg deleted file mode 100644 index 17b48a732272..000000000000 Binary files a/docs/images/yl_coder.jpg and /dev/null differ diff --git a/docs/images/yushao2.png b/docs/images/yushao2.png new file mode 100644 index 000000000000..4bd4c29fdf8a Binary files /dev/null and b/docs/images/yushao2.png differ diff --git a/docs/stylesheets/boot-cerulean.css b/docs/stylesheets/boot-cerulean.css new file mode 100644 index 000000000000..ef4412c56fee --- /dev/null +++ b/docs/stylesheets/boot-cerulean.css @@ -0,0 +1,362 @@ +/* Based on Cerulean from Bootswatch (http://bootswatch.com/cerulean/) */ + +/* document body (contains all content) */ +body { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 14px; + line-height: 1.42857143; + color: #555555; + background-color: #ffffff; + margin-left: 10%; + margin-right: 10%; +} + +/* document header (contains title etc) */ +#header { + width: 100%; +} +#header>h1 { + border-bottom: 1px solid #ddddd8; + padding-bottom: 8px; +} + +/* headings */ +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 500; + line-height: 1.2; + color: #317eac; +} +h1, +h2, +h3 { + margin-top: 20px; + margin-bottom: 10px; +} +h4, +h5, +h6 { + margin-top: 10px; + margin-bottom: 10px; +} +h1 { + font-size: 4em; +/* font-size: 36px; */ +} +h2 { + font-size: 30px; +} +h3 { + font-size: 24px; +} +h4 { + font-size: 18px; +} +h5 { + font-size: 14px; +} +h6 { + font-size: 12px; +} + +/* plain paragraph text */ +.paragraph { +/* font-family: sans-serif; */ + margin: 0 0 10px; +} +p { +/* font-family: sans-serif; */ + margin: 0 0 10px; +} + +/* blockquote text */ +.quoteblock { + font-style: italic; +} +blockquote { + padding: 10px 20px; + margin: 0 0 20px; + font-size: 17.5px; + border-left: 5px solid #eeeeee; +} +blockquote p:last-child, +blockquote ul:last-child, +blockquote ol:last-child { + margin-bottom: 0; +} +.blockquote-reverse, +blockquote.pull-right { + padding-right: 15px; + padding-left: 0; + border-right: 5px solid #eeeeee; + border-left: 0; + text-align: right; +} +.blockquote-reverse footer:before, +blockquote.pull-right footer:before, +.blockquote-reverse small:before, +blockquote.pull-right small:before, +.blockquote-reverse .small:before, +blockquote.pull-right .small:before { + content: ''; +} +.blockquote-reverse footer:after, +blockquote.pull-right footer:after, +.blockquote-reverse small:after, +blockquote.pull-right small:after, +.blockquote-reverse .small:after, +blockquote.pull-right .small:after { + content: '\00A0 \2014'; +} + +/* blockquote attribution text */ +.attribution, +.cite, +blockquote footer, +blockquote small, +blockquote .small { + display: block; + line-height: 1.42857143; + color: #999999; +} +.attribution:before, +blockquote footer:before, +blockquote small:before, +blockquote .small:before { + content: '\2014 \00A0'; +} + +/* unordered list */ +ul, ol { + margin-top: 0; + margin-bottom: 10px; +} +ul ul, +ol ul, +ul ol, +ol ol { + margin-bottom: 0; +} + + +/* links */ +a { + color: #2fa4e7; + text-decoration: none; +} +a:hover, +a:focus { + color: #157ab5; + text-decoration: underline; +} +a:focus { + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +/* horizontal rules */ +hr { + margin-top: 20px; + margin-bottom: 20px; + border: 0; + border-top: 1px solid #eeeeee; +} + +/* table */ +table { + background-color: transparent; + width: 100%; + max-width: 100%; + margin-bottom: 21px; + border-collapse: collapse; +} +table col[class*="col-"] { + position: static; + float: none; + display: table-column; +} +table td[class*="col-"], +table th[class*="col-"] { + position: static; + float: none; + display: table-cell; +} + +/* table caption */ +caption { + padding-top: 8px; + padding-bottom: 8px; + color: #999999; + text-align: left; +} + +/* table header row */ +thead { + border-bottom: 2px solid #dddddd; +} + +/* table header cell */ +th { + text-align: left; + padding-left: 8px; +} + +/* table footer */ +tfoot { + color: #807F81; + border-top: 1px solid #dddddd; +} + +/* table cell */ +td { + border-top: 1px solid #dddddd; +} +td p { + margin: auto; + padding: 8px; +} + +/* table body */ +tbody > tr:nth-of-type(odd) { + background-color: #f9f9f9; +} +tbody > tr:hover { + background-color: #f5f5f5; +} + +/* inline code */ +code, +kbd, +pre, +samp { + font-family: monospace, monospace; + font-size: 1em; +} +code { + padding: 2px 4px; + font-size: 90%; + color: #333333; + background-color: #f5f5f5; + border-radius: 4px; +} +kbd { + padding: 2px 4px; + font-size: 90%; + color: #ffffff; + background-color: #333333; + border-radius: 3px; + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25); +} +kbd kbd { + padding: 0; + font-size: 100%; + font-weight: bold; + -webkit-box-shadow: none; + box-shadow: none; +} +pre { + display: block; + padding: 9.5px; + margin: 0 0 10px; + font-size: 13px; + line-height: 1.42857143; + word-break: break-all; + word-wrap: break-word; + color: #333333; + background-color: #f5f5f5; + border: 1px solid #cccccc; + border-radius: 4px; +} +pre code { + padding: 0; + font-size: inherit; + color: inherit; + white-space: pre-wrap; + background-color: transparent; + border-radius: 0; +} + +/* image */ +img { + max-width: 100%; + vertical-align: middle; +} + +/* footer section */ +#footer { + margin-top: 22px; + padding: 14px 16px; + color: #ffffff; + background-color: #2fa4e7; +} + +/* responsiveness fixes */ +video { + max-width: 100%; +} + +/* table of Contents sidebar */ +#toctitle { + display: none; +} + +#toc { + margin-top: 20px; + background-image: -webkit-linear-gradient(#54b4eb, #2fa4e7 60%, #1d9ce5); + background-image: -o-linear-gradient(#54b4eb, #2fa4e7 60%, #1d9ce5); + background-image: -webkit-gradient(linear, left top, left bottom, from(#54b4eb), color-stop(60%, #2fa4e7), to(#1d9ce5)); + background-image: linear-gradient(#54b4eb, #2fa4e7 60%, #1d9ce5); + background-repeat: no-repeat; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff54b4eb', endColorstr='#ff1d9ce5', GradientType=0); + border-bottom: 1px solid #178acc; + -webkit-filter: none; + filter: none; + -webkit-box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1); + box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1); + border-radius: 4px 4px; +} + +#toc ul { + display: inline; + list-style-type: none; + margin: 0; + padding: 0; + overflow: hidden; +} + +#toc li { + display: block; +} + +#toc a { + color: #ffffff; + float: left; + text-align: center; + padding: 14px 16px; + text-decoration: none; +} + +#toc li a:hover { + background-color: #178acc; + text-decoration: none; +} + +#toc:after { + content: " "; + visibility: hidden; + display: block; + height: 0; + clear: both; +} + +@media all and (max-width: 600px) { +table { + width: 55vw!important; + font-size: 3vw; +}} diff --git a/docs/stylesheets/boot-cosmo.css b/docs/stylesheets/boot-cosmo.css new file mode 100644 index 000000000000..e6e3e45aacb9 --- /dev/null +++ b/docs/stylesheets/boot-cosmo.css @@ -0,0 +1,350 @@ +/* Based on Cosmo from Bootswatch (http://bootswatch.com/cosmo/) */ +@import url("https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,700"); + +/* document body (contains all content) */ +body { + font-family: "Source Sans Pro", Calibri, Candara, Arial, sans-serif; + font-size: 15px; + line-height: 1.42857143; + color: #333333; + background-color: #ffffff; + margin-left: 10%; + margin-right: 10%; +} + +/* document header (contains title etc) */ +#header { + width: 100%; +} +#header>h1 { + border-bottom: 1px solid #ddddd8; + padding-bottom: 8px; +} + +/* headings */ +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: "Source Sans Pro", Calibri, Candara, Arial, sans-serif; + font-weight: 300; + line-height: 1.1; + color: inherit; +} +h1, +h2, +h3 { + margin-top: 21px; + margin-bottom: 10.5px; +} +h4, +h5, +h6 { + margin-top: 10.5px; + margin-bottom: 10.5px; +} +h1 { +/* font-size: 39px; */ + font-size: 4em; +} +h2 { + font-size: 32px; +} +h3 { + font-size: 26px; +} +h4 { + font-size: 19px; +} +h5 { + font-size: 15px; +} +h6 { + font-size: 13px; +} + +/* plain paragraph text */ +.paragraph { +/* font-family: sans-serif; */ + margin: 0 0 10.5px; +} +p { +/* font-family: sans-serif; */ + margin: 0 0 10.5px; +} + +/* blockquote text */ +.quoteblock { + font-style: italic; +} +blockquote { + padding: 10.5px 21px; + margin: 0 0 21px; + font-size: 18.75px; + border-left: 5px solid #e6e6e6; +} +blockquote p:last-child, +blockquote ul:last-child, +blockquote ol:last-child { + margin-bottom: 0; +} +.blockquote-reverse, +blockquote.pull-right { + padding-right: 15px; + padding-left: 0; + border-right: 5px solid #e6e6e6; + border-left: 0; + text-align: right; +} +.blockquote-reverse footer:before, +blockquote.pull-right footer:before, +.blockquote-reverse small:before, +blockquote.pull-right small:before, +.blockquote-reverse .small:before, +blockquote.pull-right .small:before { + content: ''; +} +.blockquote-reverse footer:after, +blockquote.pull-right footer:after, +.blockquote-reverse small:after, +blockquote.pull-right small:after, +.blockquote-reverse .small:after, +blockquote.pull-right .small:after { + content: '\00A0 \2014'; +} + +/* the quotation mark itself (before the block) */ +.quoteblock blockquote::before { + color: blue; +} + +/* blockquote attribution text */ +.attribution, +.cite, +blockquote footer, +blockquote small, +blockquote .small { + display: block; + font-style: normal; + line-height: 1.42857143; + color: #999999; +} +.attribution:before, +blockquote footer:before, +blockquote small:before, +blockquote .small:before { + content: '\2014 \00A0'; +} + +/* unordered list */ +ul, ol { + margin-top: 0; + margin-bottom: 10.5px; +} +ul ul, +ol ul, +ul ol, +ol ol { + margin-bottom: 0; +} + + +/* links */ +a { + color: #2780e3; + text-decoration: none; +} +a:hover, +a:focus { + color: #165ba8; + text-decoration: underline; +} +a:focus { + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +/* horizontal rules */ +hr { + margin-top: 21px; + margin-bottom: 21px; + border: 0; + border-top: 1px solid #e6e6e6; +} + +/* table */ +table { + background-color: transparent; + width: 100%; + max-width: 100%; + margin-bottom: 21px; + border-collapse: collapse; +} +table col[class*="col-"] { + position: static; + float: none; + display: table-column; +} +table td[class*="col-"], +table th[class*="col-"] { + position: static; + float: none; + display: table-cell; +} + +/* table caption */ +caption { + padding-top: 8px; + padding-bottom: 8px; + color: #999999; + text-align: left; +} + +/* table header row */ +thead { + border-bottom: 2px solid #dddddd; +} + +/* table header cell */ +th { + text-align: left; + padding-left: 8px; +} + +/* table footer */ +tfoot { + color: #807F81; + border-top: 1px solid #dddddd; +} + +/* table cell */ +td { + border-top: 1px solid #dddddd; +} +td p { + margin: auto; + padding: 8px; +} + +/* table body */ +tbody > tr:nth-of-type(odd) { + background-color: #f9f9f9; +} + +/* inline code */ +code, +kbd, +pre, +samp { + font-family: Menlo, Monaco, Consolas, "Courier New", monospace; +} +code { + padding: 2px 4px; + font-size: 90%; + color: #333333; + background-color: #f5f5f5; + border-radius: 0; +} +kbd { + padding: 2px 4px; + font-size: 90%; + color: #ffffff; + background-color: #333333; + border-radius: 0; + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25); +} +kbd kbd { + padding: 0; + font-size: 100%; + font-weight: bold; + -webkit-box-shadow: none; + box-shadow: none; +} +pre { + display: block; + padding: 10px; + margin: 0 0 10.5px; + font-size: 14px; + line-height: 1.42857143; + word-break: break-all; + word-wrap: break-word; + color: #333333; + background-color: #f5f5f5; + border: 1px solid #cccccc; + border-radius: 0; +} +pre code { + padding: 0; + font-size: inherit; + color: inherit; + white-space: pre-wrap; + background-color: transparent; + border-radius: 0; +} + +/* image */ +img { + max-width: 100%; + vertical-align: middle; +} + +/* footer section */ +#footer { + margin-top: 22px; + padding: 14px 16px; + color: white; + background-color: #222222; +} + +/* responsiveness fixes */ +video { + max-width: 100%; +} + +/* table of Contents sidebar */ +#toctitle { + color: white; +} + +#toc ul { + display: inline; + list-style-type: none; + margin: 0; + padding: 0; + overflow: hidden; +} + +#toc li { + display: block; +} + +#toc a { + background-color: #2780e3; + float: left; + color: white; + text-align: center; + padding: 14px 16px; + text-decoration: none; +} + +#toc li a:hover { + background-color: #1967be; + text-decoration: none; +} + +#toc:after { + content: " "; + visibility: hidden; + display: block; + height: 0; + clear: both; +} + +@media all and (max-width: 600px) { +table { + width: 55vw!important; + font-size: 3vw; +}} diff --git a/docs/stylesheets/boot-cyborg.css b/docs/stylesheets/boot-cyborg.css new file mode 100644 index 000000000000..6d0ed086f96c --- /dev/null +++ b/docs/stylesheets/boot-cyborg.css @@ -0,0 +1,346 @@ +/* Based on Cyborg from Bootswatch (http://bootswatch.com/cyborg/) */ +@import url("https://fonts.googleapis.com/css?family=Roboto:400,700"); + +/* document body (contains all content) */ +body { + font-family: "Roboto", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 14px; + line-height: 1.42857143; + color: #888888; + background-color: #060606; + margin-left: 10%; + margin-right: 10%; +} + +/* document header (contains title etc) */ +#header { + width: 100%; +} +#header>h1 { + border-bottom: 1px solid #ddddd8; + padding-bottom: 8px; +} + +/* headings */ +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: "Roboto", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 500; + line-height: 1.1; + color: #ffffff; +} +h1, +h2, +h3 { + margin-top: 20px; + margin-bottom: 10px; +} +h4, +h5, +h6 { + margin-top: 10px; + margin-bottom: 10px; +} +h1 { + font-size: 4em; +/* font-size: 56px; */ +} +h2 { + font-size: 45px; +} +h3 { + font-size: 34px; +} +h4 { + font-size: 24px; +} +h5 { + font-size: 20px; +} +h6 { + font-size: 16px; +} + +/* plain paragraph text */ +.paragraph { +/* font-family: sans-serif; */ + margin: 0 0 10px; +} +p { + margin: 0 0 10px; +} + +/* blockquote text */ +.quoteblock { + font-style: italic; +} +blockquote { + padding: 10px 20px; + margin: 0 0 20px; + font-size: 17.5px; + border-left: 5px solid #282828; +} +blockquote p:last-child, +blockquote ul:last-child, +blockquote ol:last-child { + margin-bottom: 0; +} +.blockquote-reverse, +blockquote.pull-right { + padding-right: 15px; + padding-left: 0; + border-right: 5px solid #282828; + border-left: 0; + text-align: right; +} +.blockquote-reverse footer:before, +blockquote.pull-right footer:before, +.blockquote-reverse small:before, +blockquote.pull-right small:before, +.blockquote-reverse .small:before, +blockquote.pull-right .small:before { + content: ''; +} +.blockquote-reverse footer:after, +blockquote.pull-right footer:after, +.blockquote-reverse small:after, +blockquote.pull-right small:after, +.blockquote-reverse .small:after, +blockquote.pull-right .small:after { + content: '\00A0 \2014'; +} + +/* blockquote attribution text */ +.attribution, +.cite, +blockquote footer, +blockquote small, +blockquote .small { + display: block; + line-height: 1.42857143; + color: #555555; +} +.attribution:before, +blockquote footer:before, +blockquote small:before, +blockquote .small:before { + content: '\2014 \00A0'; +} + +/* unordered list */ +ul, ol { + margin-top: 0; + margin-bottom: 10px; +} +ul ul, +ol ul, +ul ol, +ol ol { + margin-bottom: 0; +} + +/* links */ +a { + color: #2a9fd6; + text-decoration: none; +} +a:hover, +a:focus { + color: #2a9fd6; + text-decoration: underline; +} +a:focus { + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +/* horizontal rules */ +hr { + margin-top: 20px; + margin-bottom: 20px; + border: 0; + border-top: 1px solid #282828; +} + +/* table */ +table { + background-color: transparent; + width: 100%; + max-width: 100%; + margin-bottom: 21px; + border-collapse: collapse; +} +table col[class*="col-"] { + position: static; + float: none; + display: table-column; +} +table td[class*="col-"], +table th[class*="col-"] { + position: static; + float: none; + display: table-cell; +} + +/* table caption */ +caption { + padding-top: 8px; + padding-bottom: 8px; + color: #888888; + text-align: left; +} + +/* table header row */ +thead { + border-bottom: 2px solid #282828; +} + +/* table header cell */ +th { + text-align: left; + padding-left: 8px; +} + +/* table footer */ +tfoot { + color: #807F81; + border-top: 1px solid #282828; +} + +/* table cell */ +td { + border-top: 1px solid #282828; +} +td p { + margin: auto; + padding: 8px; +} + +/* table body */ +tbody > tr:nth-of-type(odd) { + background-color: #080808; +} +tbody > tr:hover { + background-color: #282828; +} + +/* inline code */ +code, +kbd, +pre, +samp { + font-family: monospace, monospace; + font-size: 1em; +} +code { + padding: 2px 4px; + font-size: 90%; + color: #282828; + background-color: #f5f5f5; + border-radius: 4px; +} +kbd { + padding: 2px 4px; + font-size: 90%; + color: #ffffff; + background-color: #333333; + border-radius: 3px; + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25); +} +kbd kbd { + padding: 0; + font-size: 100%; + font-weight: bold; + -webkit-box-shadow: none; + box-shadow: none; +} +pre { + display: block; + padding: 9.5px; + margin: 0 0 10px; + font-size: 13px; + line-height: 1.42857143; + word-break: break-all; + word-wrap: break-word; + color: #282828; + background-color: #f5f5f5; + border: 1px solid #cccccc; + border-radius: 4px; +} +pre code { + padding: 0; + font-size: inherit; + color: inherit; + white-space: pre-wrap; + background-color: transparent; + border-radius: 0; +} + +/* image */ +img { + max-width: 100%; + vertical-align: middle; +} + +/* footer section */ +#footer { + margin-top: 22px; + padding: 14px 16px; + background-color: #060606; + border-color: #282828; +} + +/* responsiveness fixes */ +video { + max-width: 100%; +} + +/* table of Contents sidebar */ +#toctitle { + color: #060606; +} + +#toc ul { + display: inline; + list-style-type: none; + margin: 0; + padding: 0; + overflow: hidden; +} + +#toc li { + display: block; +} + +#toc a { + background-color: #060606; + float: left; + color: #888888; + text-align: center; + padding: 14px 16px; + text-decoration: none; +} + +#toc li a:hover { + color: #ffffff; + text-decoration: none; +} + +#toc:after { + content: " "; + visibility: hidden; + display: block; + height: 0; + clear: both; +} + +@media all and (max-width: 600px) { +table { + width: 55vw!important; + font-size: 3vw; +}} diff --git a/docs/stylesheets/boot-darkly.css b/docs/stylesheets/boot-darkly.css new file mode 100644 index 000000000000..81c394443b53 --- /dev/null +++ b/docs/stylesheets/boot-darkly.css @@ -0,0 +1,349 @@ +/* Based on Darkly from Bootswatch (http://bootswatch.com/darkly/) */ +@import url("https://fonts.googleapis.com/css?family=Lato:400,700,400italic"); + +/* document body (contains all content) */ +body { + font-family: "Lato", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 15px; + line-height: 1.42857143; + color: #ffffff; + background-color: #222222; + margin-left: 10%; + margin-right: 10%; +} + +/* document header (contains title etc) */ +#header { + width: 100%; +} +#header>h1 { + border-bottom: 1px solid #ddddd8; + padding-bottom: 8px; +} + +/* headings */ +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: "Lato", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 400; + line-height: 1.1; + color: inherit; +} +h1, +h2, +h3 { + margin-top: 21px; + margin-bottom: 10.5px; +} +h4, +h5, +h6 { + margin-top: 10.5px; + margin-bottom: 10.5px; +} +h1 { +/* font-size: 39px; */ + font-size: 4em; +} +h2 { + font-size: 32px; +} +h3 { + font-size: 26px; +} +h4 { + font-size: 19px; +} +h5 { + font-size: 15px; +} +h6 { + font-size: 13px; +} + +/* plain paragraph text */ +.paragraph { +/* font-family: sans-serif; */ + margin: 0 0 10.5px; +} +p { +/* font-family: sans-serif; */ + margin: 0 0 10.5px; +} + +/* blockquote text */ +.quoteblock { + font-style: italic; +} +blockquote { + padding: 10.5px 21px; + margin: 0 0 21px; + font-size: 18.75px; + border-left: 5px solid #464545; +} +blockquote p:last-child, +blockquote ul:last-child, +blockquote ol:last-child { + margin-bottom: 0; +} +.blockquote-reverse, +blockquote.pull-right { + padding-right: 15px; + padding-left: 0; + border-right: 5px solid #464545; + border-left: 0; + text-align: right; +} +.blockquote-reverse footer:before, +blockquote.pull-right footer:before, +.blockquote-reverse small:before, +blockquote.pull-right small:before, +.blockquote-reverse .small:before, +blockquote.pull-right .small:before { + content: ''; +} +.blockquote-reverse footer:after, +blockquote.pull-right footer:after, +.blockquote-reverse small:after, +blockquote.pull-right small:after, +.blockquote-reverse .small:after, +blockquote.pull-right .small:after { + content: '\00A0 \2014'; +} + +/* blockquote attribution text */ +.attribution, +.cite, +blockquote footer, +blockquote small, +blockquote .small { + display: block; + font-style: normal; + line-height: 1.42857143; + color: #999999; +} +.attribution:before, +blockquote footer:before, +blockquote small:before, +blockquote .small:before { + content: '\2014 \00A0'; +} + +/* unordered list */ +ul, ol { + margin-top: 0; + margin-bottom: 10.5px; +} +ul ul, +ol ul, +ul ol, +ol ol { + margin-bottom: 0; +} + + +/* links */ +a { + color: rgb(12, 227, 172); + background-color: transparent; + text-decoration: none; +} +a:hover, +a:focus { + color: #0ce3ac; + text-decoration: underline; +} +a:focus { + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +/* horizontal rules */ +hr { + margin-top: 21px; + margin-bottom: 21px; + border: 0; + border-top: 1px solid #464545; +} + +/* table */ +table { + background-color: transparent; + width: 100%; + max-width: 100%; + margin-bottom: 21px; + border-collapse: collapse; +} +table col[class*="col-"] { + position: static; + float: none; + display: table-column; +} +table td[class*="col-"], +table th[class*="col-"] { + position: static; + float: none; + display: table-cell; +} + +/* table caption */ +caption { + padding-top: 8px; + padding-bottom: 8px; + color: #999999; + text-align: left; +} + +/* table header row */ +thead { + border-bottom: 2px solid #464545; +} + +/* table header cell */ +th { + text-align: left; + padding-left: 8px; +} + +/* table footer */ +tfoot { + color: #807F81; + border-top: 1px solid #464545; +} + +/* table cell */ +td { + border-top: 1px solid #464545; +} +td p { + margin: auto; + padding: 8px; +} + +/* table body */ +tbody > tr:nth-of-type(odd) { + background-color: #3d3d3d; +} +tbody > tr:hover { + background-color: #464545; +} + +/* inline code */ +code, +kbd, +pre, +samp { + font-family: Menlo, Monaco, Consolas, "Courier New", monospace; +} +code { + padding: 2px 4px; + font-size: 90%; + color: #c7254e; + background-color: #f9f2f4; + border-radius: 4px; +} +kbd { + padding: 2px 4px; + font-size: 90%; + color: #ffffff; + background-color: #333333; + border-radius: 0; + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25); +} +kbd kbd { + padding: 0; + font-size: 100%; + font-weight: bold; + -webkit-box-shadow: none; + box-shadow: none; +} +pre { + display: block; + padding: 10px; + margin: 0 0 10.5px; + font-size: 14px; + line-height: 1.42857143; + word-break: break-all; + word-wrap: break-word; + color: #303030; + background-color: #ebebeb; + border: 1px solid #cccccc; + border-radius: 4px; +} +pre code { + padding: 0; + font-size: inherit; + color: inherit; + white-space: pre-wrap; + background-color: transparent; + border-radius: 0; +} + +/* image */ +img { + max-width: 100%; + vertical-align: middle; +} + +/* footer section */ +#footer { + margin-top: 22px; + padding: 14px 16px; + color: white; + background-color: #375A7F; +} + +/* responsiveness fixes */ +video { + max-width: 100%; +} + +/* table of Contents sidebar */ +#toctitle { + color: #222222; +} + +#toc ul { + display: inline; + list-style-type: none; + margin: 0; + padding: 0; + overflow: hidden; +} + +#toc li { + display: block; +} + +#toc a { + background-color: rgb(55, 90, 127); + float: left; + color: white; + text-align: center; + padding: 14px 16px; + text-decoration: none; +} + +#toc li a:hover { + background-color: rgb(40, 65, 91); + text-decoration: none; +} + +#toc:after { + content: " "; + visibility: hidden; + display: block; + height: 0; + clear: both; +} + +@media all and (max-width: 600px) { +table { + width: 55vw!important; + font-size: 3vw; +}} diff --git a/docs/stylesheets/boot-flatly.css b/docs/stylesheets/boot-flatly.css new file mode 100644 index 000000000000..4640c2368057 --- /dev/null +++ b/docs/stylesheets/boot-flatly.css @@ -0,0 +1,350 @@ +/* Based on Flatly from Bootswatch (http://bootswatch.com/flatly/) */ +@import url("https://fonts.googleapis.com/css?family=Lato:400,700,400italic"); + +/* document body (contains all content) */ +body { + font-family: "Lato", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 15px; + line-height: 1.42857143; + color: #2c3e50; + background-color: #ffffff; + margin-left: 10%; + margin-right: 10%; +} + +/* document header (contains title etc) */ +#header { + width: 100%; +} +#header>h1 { + border-bottom: 1px solid #ddddd8; + padding-bottom: 8px; +} + + +/* headings */ +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: "Lato", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 400; + line-height: 1.1; + color: inherit; +} +h1, +h2, +h3 { + margin-top: 21px; + margin-bottom: 10.5px; +} +h4, +h5, +h6 { + margin-top: 10.5px; + margin-bottom: 10.5px; +} +h1 { + font-size: 4em; +/* font-size: 39px; */ +} +h2 { + font-size: 32px; +} +h3 { + font-size: 26px; +} +h4 { + font-size: 19px; +} +h5 { + font-size: 15px; +} +h6 { + font-size: 13px; +} + +/* plain paragraph text */ +.paragraph { +/* font-family: sans-serif; */ + margin: 0 0 10.5px; +} +p { +/* font-family: sans-serif; */ + margin: 0 0 10.5px; +} + +/* blockquote text */ +.quoteblock { + font-style: italic; +} +blockquote { + padding: 10.5px 21px; + margin: 0 0 21px; + font-size: 18.75px; + border-left: 5px solid #ecf0f1; +} +blockquote p:last-child, +blockquote ul:last-child, +blockquote ol:last-child { + margin-bottom: 0; +} +.blockquote-reverse, +blockquote.pull-right { + padding-right: 15px; + padding-left: 0; + border-right: 5px solid #ecf0f1; + border-left: 0; + text-align: right; +} +.blockquote-reverse footer:before, +blockquote.pull-right footer:before, +.blockquote-reverse small:before, +blockquote.pull-right small:before, +.blockquote-reverse .small:before, +blockquote.pull-right .small:before { + content: ''; +} +.blockquote-reverse footer:after, +blockquote.pull-right footer:after, +.blockquote-reverse small:after, +blockquote.pull-right small:after, +.blockquote-reverse .small:after, +blockquote.pull-right .small:after { + content: '\00A0 \2014'; +} + +/* blockquote attribution text */ +.attribution, +.cite, +blockquote footer, +blockquote small, +blockquote .small { + display: block; + font-size: 80%; + line-height: 1.42857143; + color: #b4bcc2; +} +.attribution:before, +blockquote footer:before, +blockquote small:before, +blockquote .small:before { + content: '\2014 \00A0'; +} + +/* unordered list */ +ul, ol { + margin-top: 0; + margin-bottom: 10.5px; +} +ul ul, +ol ul, +ul ol, +ol ol { + margin-bottom: 0; +} + + +/* links */ +a { + color: #18bc9c; + background-color: transparent; + text-decoration: none; +} +a:hover, +a:focus { + color: #18bc9c; + text-decoration: underline; +} +a:focus { + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +/* horizontal rules */ +hr { + margin-top: 21px; + margin-bottom: 21px; + border: 0; + border-top: 1px solid #ecf0f1; +} + +/* table */ +table { + background-color: transparent; + width: 100%; + max-width: 100%; + margin-bottom: 21px; + border-collapse: collapse; +} +table col[class*="col-"] { + position: static; + float: none; + display: table-column; +} +table td[class*="col-"], +table th[class*="col-"] { + position: static; + float: none; + display: table-cell; +} + +/* table caption */ +caption { + padding-top: 8px; + padding-bottom: 8px; + color: #b4bcc2; + text-align: left; +} + +/* table header row */ +thead { + border-bottom: 2px solid #ecf0f1; +} + +/* table header cell */ +th { + text-align: left; + padding-left: 8px; +} + +/* table footer */ +tfoot { + color: #807F81; + border-top: 1px solid #ecf0f1; +} + +/* table cell */ +td { + border-top: 1px solid #ecf0f1; +} +td p { + margin: auto; + padding: 8px; +} + +/* table body */ +tbody > tr:nth-of-type(odd) { + background-color: #f9f9f9; +} +tbody > tr:hover { + background-color: #ecf0f1; +} + +/* inline code */ +code, +kbd, +pre, +samp { + font-family: Menlo, Monaco, Consolas, "Courier New", monospace; +} +code { + padding: 2px 4px; + font-size: 90%; + color: #c7254e; + background-color: #f9f2f4; + border-radius: 4px; +} +kbd { + padding: 2px 4px; + font-size: 90%; + color: #ffffff; + background-color: #333333; + border-radius: 0; + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25); +} +kbd kbd { + padding: 0; + font-size: 100%; + font-weight: bold; + -webkit-box-shadow: none; + box-shadow: none; +} +pre { + display: block; + padding: 10.5px; + margin: 0 0 10.5px; + font-size: 15px; + line-height: 1.42857143; + word-break: break-all; + word-wrap: break-word; + color: #333333; + background-color: #f5f5f5; + border: 1px solid #cccccc; + border-radius: 4px; +} +pre code { + padding: 0; + font-size: inherit; + color: inherit; + white-space: pre-wrap; + background-color: transparent; + border-radius: 0; +} + +/* image */ +img { + max-width: 100%; + vertical-align: middle; +} + +/* footer section */ +#footer { + margin-top: 22px; + padding: 14px 16px; + color: white; + background-color: #2c3e50; +} + +/* responsiveness fixes */ +video { + max-width: 100%; +} + +/* table of Contents sidebar */ +#toctitle { + color: #ffffff; +} + +#toc ul { + display: inline; + list-style-type: none; + margin: 0; + padding: 0; + overflow: hidden; +} + +#toc li { + display: block; +} + +#toc a { + background-color: #2c3e50; + float: left; + color: white; + text-align: center; + padding: 14px 16px; + text-decoration: none; +} + +#toc li a:hover { + background-color: #1a242f; + text-decoration: none; +} + +#toc:after { + content: " "; + visibility: hidden; + display: block; + height: 0; + clear: both; +} + +@media all and (max-width: 600px) { +table { + width: 55vw!important; + font-size: 3vw; +}} diff --git a/docs/stylesheets/boot-journal.css b/docs/stylesheets/boot-journal.css new file mode 100644 index 000000000000..f5746339d90d --- /dev/null +++ b/docs/stylesheets/boot-journal.css @@ -0,0 +1,351 @@ +/* Based on Journal from Bootswatch (http://bootswatch.com/flatly/) */ +@import url("https://fonts.googleapis.com/css?family=News+Cycle:400,700"); + +/* document body (contains all content) */ +body { + font-family: Georgia, "Times New Roman", Times, serif; + font-size: 15px; + line-height: 1.42857143; + color: #777777; + background-color: #ffffff; + margin-left: 10%; + margin-right: 10%; +} + +/* document header (contains title etc) */ +#header { + width: 100%; +} +#header>h1 { + border-bottom: 1px solid #ddddd8; + padding-bottom: 8px; +} + + +/* headings */ +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: "News Cycle", "Arial Narrow Bold", sans-serif; + font-weight: 700; + line-height: 1.1; + color: #000000; +} +h1, +h2, +h3 { + margin-top: 21px; + margin-bottom: 10.5px; +} +h4, +h5, +h6 { + margin-top: 10.5px; + margin-bottom: 10.5px; +} +h1 { + font-size: 4em; +/* font-size: 39px; */ +} +h2 { + font-size: 32px; +} +h3 { + font-size: 26px; +} +h4 { + font-size: 19px; +} +h5 { + font-size: 15px; +} +h6 { + font-size: 13px; +} + +/* plain paragraph text */ +.paragraph { +/* font-family: sans-serif; */ + margin: 0 0 10.5px; +} +p { +/* font-family: sans-serif; */ + margin: 0 0 10.5px; +} + +/* blockquote text */ +.quoteblock { + font-style: italic; +} +blockquote { + padding: 10.5px 21px; + margin: 0 0 21px; + font-size: 18.75px; + border-left: 5px solid #eeeeee; +} +blockquote p:last-child, +blockquote ul:last-child, +blockquote ol:last-child { + margin-bottom: 0; +} +.blockquote-reverse, +blockquote.pull-right { + padding-right: 15px; + padding-left: 0; + border-right: 5px solid #eeeeee; + border-left: 0; + text-align: right; +} +.blockquote-reverse footer:before, +blockquote.pull-right footer:before, +.blockquote-reverse small:before, +blockquote.pull-right small:before, +.blockquote-reverse .small:before, +blockquote.pull-right .small:before { + content: ''; +} +.blockquote-reverse footer:after, +blockquote.pull-right footer:after, +.blockquote-reverse small:after, +blockquote.pull-right small:after, +.blockquote-reverse .small:after, +blockquote.pull-right .small:after { + content: '\00A0 \2014'; +} + +/* blockquote attribution text */ +.attribution, +.cite, +blockquote footer, +blockquote small, +blockquote .small { + display: block; + font-size: 80%; + line-height: 1.42857143; + color: #999999; +} +.attribution:before, +blockquote footer:before, +blockquote small:before, +blockquote .small:before { + content: '\2014 \00A0'; +} + +/* unordered list */ +ul, ol { + margin-top: 0; + margin-bottom: 10.5px; +} +ul ul, +ol ul, +ul ol, +ol ol { + margin-bottom: 0; +} + + +/* links */ +a { + color: #18bc9c; + background-color: transparent; + text-decoration: none; +} +a:hover, +a:focus { + color: #e22620; + text-decoration: underline; +} +a:focus { + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +/* horizontal rules */ +hr { + margin-top: 21px; + margin-bottom: 21px; + border: 0; + border-top: 1px solid #eeeeee; +} + +/* table */ +table { + background-color: transparent; + width: 100%; + max-width: 100%; + margin-bottom: 21px; + border-collapse: collapse; +} +table col[class*="col-"] { + position: static; + float: none; + display: table-column; +} +table td[class*="col-"], +table th[class*="col-"] { + position: static; + float: none; + display: table-cell; +} + +/* table caption */ +caption { + padding-top: 8px; + padding-bottom: 8px; + color: #999999; + text-align: left; +} + +/* table header row */ +thead { + border-bottom: 2px solid #dddddd; +} + +/* table header cell */ +th { + text-align: left; + padding-left: 8px; +} + +/* table footer */ +tfoot { + color: #807F81; + border-top: 1px solid #dddddd; +} + +/* table cell */ +td { + border-top: 1px solid #dddddd; +} +td p { + margin: auto; + padding: 8px; +} + +/* table body */ +tbody > tr:nth-of-type(odd) { + background-color: #f9f9f9; +} +tbody > tr:hover { + background-color: #f5f5f5; +} + +/* inline code */ +code, +kbd, +pre, +samp { + font-family: monospace, monospace; + font-size: 1em; +} +code { + padding: 2px 4px; + font-size: 90%; + color: #333333; + background-color: #f5f5f5; + border-radius: 4px; +} +kbd { + padding: 2px 4px; + font-size: 90%; + color: #ffffff; + background-color: #333333; + border-radius: 3px; + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25); +} +kbd kbd { + padding: 0; + font-size: 100%; + font-weight: bold; + -webkit-box-shadow: none; + box-shadow: none; +} +pre { + display: block; + padding: 10.5px; + margin: 0 0 10.5px; + font-size: 15px; + line-height: 1.42857143; + word-break: break-all; + word-wrap: break-word; + color: #333333; + background-color: #f5f5f5; + border: 1px solid #cccccc; + border-radius: 4px; +} +pre code { + padding: 0; + font-size: inherit; + color: inherit; + white-space: pre-wrap; + background-color: transparent; + border-radius: 0; +} + +/* image */ +img { + max-width: 100%; + vertical-align: middle; +} + +/* footer section */ +#footer { + margin-top: 22px; + padding: 14px 16px; + color: #777777; + background-color: #ffffff; +} + +/* responsiveness fixes */ +video { + max-width: 100%; +} + +/* table of Contents sidebar */ +#toctitle { + color: #ffffff; +} + +#toc ul { + display: inline; + list-style-type: none; + margin: 0; + padding: 0; + overflow: hidden; +} + +#toc li { + display: block; +} + +#toc a { + background-color: #ffffff; + float: left; + color: #777777; + text-align: center; + padding: 14px 16px; + text-decoration: none; +} + +#toc li a:hover { + background-color: #eeeeee; + text-decoration: none; +} + +#toc:after { + content: " "; + visibility: hidden; + display: block; + height: 0; + clear: both; +} + +@media all and (max-width: 600px) { +table { + width: 55vw!important; + font-size: 3vw; +}} diff --git a/docs/stylesheets/boot-lumen-dominic.css b/docs/stylesheets/boot-lumen-dominic.css new file mode 100644 index 000000000000..bcb391faf5e7 --- /dev/null +++ b/docs/stylesheets/boot-lumen-dominic.css @@ -0,0 +1,361 @@ +/* Based on Lumen from Bootswatch (http://bootswatch.com/lumen/) */ +@import url("https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,700,400italic"); +@import url("https://fonts.googleapis.com/css?family=Ubuntu"); +@import url("https://fonts.googleapis.com/css?family=Cookie"); +@import url("https://fonts.googleapis.com/css?family=Enriqueta"); +@import url("https://fonts.googleapis.com/css?family=Kumar+One+Outline"); + + +/* document body (contains all content) */ +body { + font-family: "Ubuntu",sans-serif; + font-size: 12px; + line-height: 1.45; + color: #2C001E; + background-color: #ffffff; + margin-left: 10%; + margin-right: 10%; +} + +/* document header (contains title etc) */ +#header { + width: 100%; +} +#header>h1 { + border-bottom: 5px solid #2C001E; + padding-bottom: 5px; +} + + +/* headings */ +h1 { + font-family: 'Cookie', cursive; + font-weight: 400; + line-height: 1.1; + color: #333333; +} +h2, +h3, +h4, +h5, +h6 { + font-family: 'Enriqueta', serif;; + font-weight: 400; + line-height: 1.1; + color: #333333; +} +h1, +h2, +h3 { + margin-top: 3px; + margin-bottom: 3px; +} +h4, +h5, +h6 { + margin-top: 3px; + margin-bottom: 3px; +} +h1 { + font-size: 40px; +/* font-size: 36/3 * 2.5px; */ +} +h2 { + font-size: 20px; +} +h3 { + font-size: 17px; +} +h4 { + font-size: 15px; +} +h5 { + font-size: 14px; +} +h6 { + font-size: 14px; +} + +/* plain paragraph text */ +.paragraph { +/* font-family: sans-serif; */ + margin: 0 0 1px; +} +p { +/* font-family: sans-serif; */ + margin: 0 0 1px; +} + +/* blockquote text */ +.quoteblock { + font-style: italic; +} +blockquote { + padding: 3px 3px; + margin: 0 0 3px; + font-size: 10px; + border-left: 3px solid #b3ffff; +} +blockquote p:last-child, +blockquote ul:last-child, +blockquote ol:last-child { + margin-bottom: 0; +} +.blockquote-reverse, +blockquote.pull-right { + padding-right: 10px; + padding-left: 0; + border-right: 3px solid #b3ffff; + border-left: 0; + text-align: right; +} +.blockquote-reverse footer:before, +blockquote.pull-right footer:before, +.blockquote-reverse small:before, +blockquote.pull-right small:before, +.blockquote-reverse .small:before, +blockquote.pull-right .small:before { + content: ''; +} +.blockquote-reverse footer:after, +blockquote.pull-right footer:after, +.blockquote-reverse small:after, +blockquote.pull-right small:after, +.blockquote-reverse .small:after, +blockquote.pull-right .small:after { + content: '\00A0 \2014'; +} + +/* blockquote attribution text */ +.attribution, +.cite, +blockquote footer, +blockquote small, +blockquote .small { + display: block; + font-size: 80%; + line-height: 1.42857143; + color: #2C001E; +} +.attribution:before, +blockquote footer:before, +blockquote small:before, +blockquote .small:before { + content: '\2014 \00A0'; +} + +/* unordered list */ +ul, ol { + margin-top: 0; + margin-bottom: 3px; +} +ul ul, +ol ul, +ul ol, +ol ol { + margin-bottom: 0; +} + + +/* links */ +a { + color: #158cba; + background-color: transparent; + text-decoration: none; +} +a:hover, +a:focus { + color: #158cba; + text-decoration: underline; +} +a:focus { + outline: 3px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +/* horizontal rules */ +hr { + margin-top: 3px; + margin-bottom: 3px; + border: 0; + border-top: 1px solid #e6e6ff; +} + +/* table */ +table { + background-color: transparent; + width: 100%; + max-width: 100%; + margin-bottom: 3px; + border-collapse: collapse; +} +table col[class*="col-"] { + position: static; + float: none; + display: table-column; +} +table td[class*="col-"], +table th[class*="col-"] { + position: static; + float: none; + display: table-cell; +} + +/* table caption */ +caption { + padding-top: 3px; + padding-bottom: 3px; + color: #006666; + text-align: left; +} + +/* table header row */ +thead { + border-bottom: 1px solid #e6e6ff; +} + +/* table header cell */ +th { + text-align: left; + padding-left: 1px; +} + +/* table footer */ +tfoot { + color: #2C001E; + border-top: 1px solid #e6e6ff; +} + +/* table cell */ +td { + border-top: 1px solid #e6e6ff; +} +td p { + margin: auto; + padding: 1px; +} + +/* table body */ +tbody > tr:nth-of-type(odd) { + background-color: #ccffe6; +} +tbody > tr:hover { + background-color: #ccffe6; +} + +/* inline code */ +code, +kbd, +pre, +samp { + font-family: "Ubuntu",sans-serif; + font-size: 1em; +} +code { + padding: 2px 2px; + font-size: 90%; + color: #ffffff; + background-color: #006666; + border-radius: 3px; +} +kbd { + padding: 2px 3px; + font-size: 90%; + color: #ccffe6; + background-color: #333333; + border-radius: 2px; + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25); +} +kbd kbd { + padding: 0; + font-size: 100%; + font-weight: bold; + -webkit-box-shadow: none; + box-shadow: none; +} +pre { + display: block; + padding: 5px; + margin: 0 0 3px; + font-size: 10px; + line-height: 1.42857143; + word-break: break-all; + word-wrap: break-word; + color: #333333; + background-color: #ccffe6; + border: 1px solid #ccffe6; + border-radius: 3px; +} +pre code { + padding: 0; + font-size: inherit; + color: inherit; + white-space: pre-wrap; + background-color: transparent; + border-radius: 0; +} + +/* image */ +img { + max-width: 100%; + vertical-align: middle; +} + +/* footer section */ +#footer { + margin-top: 10px; + padding: 3px 3px; + color: #2C001E; + background-color: #ccffe6; +} + +/* responsiveness fixes */ +video { + max-width: 100%; +} + +/* table of Contents sidebar */ +#toctitle { + color: #ccffe6; +} + +#toc ul { + display: inline; + list-style-type: none; + margin: 0; + padding: 0; + overflow: hidden; +} + +#toc li { + display: block; +} + +#toc a { + background-color: #ccffe6; + float: left; + color: #2C001E; + text-align: center; + padding: 3px 3px; + text-decoration: none; +} + +#toc li a:hover { + background-color: #ccffe6; + text-decoration: none; +} + +#toc:after { + content: " "; + visibility: hidden; + display: block; + height: 0; + clear: both; +} + +@media all and (max-width: 600px) { +table { + width: 55vw!important; + font-size: 3vw; +}} diff --git a/docs/stylesheets/boot-lumen-yushao.css b/docs/stylesheets/boot-lumen-yushao.css new file mode 100644 index 000000000000..bcb391faf5e7 --- /dev/null +++ b/docs/stylesheets/boot-lumen-yushao.css @@ -0,0 +1,361 @@ +/* Based on Lumen from Bootswatch (http://bootswatch.com/lumen/) */ +@import url("https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,700,400italic"); +@import url("https://fonts.googleapis.com/css?family=Ubuntu"); +@import url("https://fonts.googleapis.com/css?family=Cookie"); +@import url("https://fonts.googleapis.com/css?family=Enriqueta"); +@import url("https://fonts.googleapis.com/css?family=Kumar+One+Outline"); + + +/* document body (contains all content) */ +body { + font-family: "Ubuntu",sans-serif; + font-size: 12px; + line-height: 1.45; + color: #2C001E; + background-color: #ffffff; + margin-left: 10%; + margin-right: 10%; +} + +/* document header (contains title etc) */ +#header { + width: 100%; +} +#header>h1 { + border-bottom: 5px solid #2C001E; + padding-bottom: 5px; +} + + +/* headings */ +h1 { + font-family: 'Cookie', cursive; + font-weight: 400; + line-height: 1.1; + color: #333333; +} +h2, +h3, +h4, +h5, +h6 { + font-family: 'Enriqueta', serif;; + font-weight: 400; + line-height: 1.1; + color: #333333; +} +h1, +h2, +h3 { + margin-top: 3px; + margin-bottom: 3px; +} +h4, +h5, +h6 { + margin-top: 3px; + margin-bottom: 3px; +} +h1 { + font-size: 40px; +/* font-size: 36/3 * 2.5px; */ +} +h2 { + font-size: 20px; +} +h3 { + font-size: 17px; +} +h4 { + font-size: 15px; +} +h5 { + font-size: 14px; +} +h6 { + font-size: 14px; +} + +/* plain paragraph text */ +.paragraph { +/* font-family: sans-serif; */ + margin: 0 0 1px; +} +p { +/* font-family: sans-serif; */ + margin: 0 0 1px; +} + +/* blockquote text */ +.quoteblock { + font-style: italic; +} +blockquote { + padding: 3px 3px; + margin: 0 0 3px; + font-size: 10px; + border-left: 3px solid #b3ffff; +} +blockquote p:last-child, +blockquote ul:last-child, +blockquote ol:last-child { + margin-bottom: 0; +} +.blockquote-reverse, +blockquote.pull-right { + padding-right: 10px; + padding-left: 0; + border-right: 3px solid #b3ffff; + border-left: 0; + text-align: right; +} +.blockquote-reverse footer:before, +blockquote.pull-right footer:before, +.blockquote-reverse small:before, +blockquote.pull-right small:before, +.blockquote-reverse .small:before, +blockquote.pull-right .small:before { + content: ''; +} +.blockquote-reverse footer:after, +blockquote.pull-right footer:after, +.blockquote-reverse small:after, +blockquote.pull-right small:after, +.blockquote-reverse .small:after, +blockquote.pull-right .small:after { + content: '\00A0 \2014'; +} + +/* blockquote attribution text */ +.attribution, +.cite, +blockquote footer, +blockquote small, +blockquote .small { + display: block; + font-size: 80%; + line-height: 1.42857143; + color: #2C001E; +} +.attribution:before, +blockquote footer:before, +blockquote small:before, +blockquote .small:before { + content: '\2014 \00A0'; +} + +/* unordered list */ +ul, ol { + margin-top: 0; + margin-bottom: 3px; +} +ul ul, +ol ul, +ul ol, +ol ol { + margin-bottom: 0; +} + + +/* links */ +a { + color: #158cba; + background-color: transparent; + text-decoration: none; +} +a:hover, +a:focus { + color: #158cba; + text-decoration: underline; +} +a:focus { + outline: 3px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +/* horizontal rules */ +hr { + margin-top: 3px; + margin-bottom: 3px; + border: 0; + border-top: 1px solid #e6e6ff; +} + +/* table */ +table { + background-color: transparent; + width: 100%; + max-width: 100%; + margin-bottom: 3px; + border-collapse: collapse; +} +table col[class*="col-"] { + position: static; + float: none; + display: table-column; +} +table td[class*="col-"], +table th[class*="col-"] { + position: static; + float: none; + display: table-cell; +} + +/* table caption */ +caption { + padding-top: 3px; + padding-bottom: 3px; + color: #006666; + text-align: left; +} + +/* table header row */ +thead { + border-bottom: 1px solid #e6e6ff; +} + +/* table header cell */ +th { + text-align: left; + padding-left: 1px; +} + +/* table footer */ +tfoot { + color: #2C001E; + border-top: 1px solid #e6e6ff; +} + +/* table cell */ +td { + border-top: 1px solid #e6e6ff; +} +td p { + margin: auto; + padding: 1px; +} + +/* table body */ +tbody > tr:nth-of-type(odd) { + background-color: #ccffe6; +} +tbody > tr:hover { + background-color: #ccffe6; +} + +/* inline code */ +code, +kbd, +pre, +samp { + font-family: "Ubuntu",sans-serif; + font-size: 1em; +} +code { + padding: 2px 2px; + font-size: 90%; + color: #ffffff; + background-color: #006666; + border-radius: 3px; +} +kbd { + padding: 2px 3px; + font-size: 90%; + color: #ccffe6; + background-color: #333333; + border-radius: 2px; + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25); +} +kbd kbd { + padding: 0; + font-size: 100%; + font-weight: bold; + -webkit-box-shadow: none; + box-shadow: none; +} +pre { + display: block; + padding: 5px; + margin: 0 0 3px; + font-size: 10px; + line-height: 1.42857143; + word-break: break-all; + word-wrap: break-word; + color: #333333; + background-color: #ccffe6; + border: 1px solid #ccffe6; + border-radius: 3px; +} +pre code { + padding: 0; + font-size: inherit; + color: inherit; + white-space: pre-wrap; + background-color: transparent; + border-radius: 0; +} + +/* image */ +img { + max-width: 100%; + vertical-align: middle; +} + +/* footer section */ +#footer { + margin-top: 10px; + padding: 3px 3px; + color: #2C001E; + background-color: #ccffe6; +} + +/* responsiveness fixes */ +video { + max-width: 100%; +} + +/* table of Contents sidebar */ +#toctitle { + color: #ccffe6; +} + +#toc ul { + display: inline; + list-style-type: none; + margin: 0; + padding: 0; + overflow: hidden; +} + +#toc li { + display: block; +} + +#toc a { + background-color: #ccffe6; + float: left; + color: #2C001E; + text-align: center; + padding: 3px 3px; + text-decoration: none; +} + +#toc li a:hover { + background-color: #ccffe6; + text-decoration: none; +} + +#toc:after { + content: " "; + visibility: hidden; + display: block; + height: 0; + clear: both; +} + +@media all and (max-width: 600px) { +table { + width: 55vw!important; + font-size: 3vw; +}} diff --git a/docs/stylesheets/boot-lumen.css b/docs/stylesheets/boot-lumen.css new file mode 100644 index 000000000000..123ef8c52aaf --- /dev/null +++ b/docs/stylesheets/boot-lumen.css @@ -0,0 +1,351 @@ +/* Based on Lumen from Bootswatch (http://bootswatch.com/lumen/) */ +@import url("https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,700,400italic"); + +/* document body (contains all content) */ +body { + font-family: "Source Sans Pro", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 14px; + line-height: 1.42857143; + color: #555555; + background-color: #ffffff; + margin-left: 10%; + margin-right: 10%; +} + +/* document header (contains title etc) */ +#header { + width: 100%; +} +#header>h1 { + border-bottom: 1px solid #ddddd8; + padding-bottom: 8px; +} + + +/* headings */ +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: inherit; + font-weight: 400; + line-height: 1.1; + color: #333333; +} +h1, +h2, +h3 { + margin-top: 20px; + margin-bottom: 10px; +} +h4, +h5, +h6 { + margin-top: 10px; + margin-bottom: 10px; +} +h1 { + font-size: 3em; +/* font-size: 36px; */ +} +h2 { + font-size: 30px; +} +h3 { + font-size: 24px; +} +h4 { + font-size: 18px; +} +h5 { + font-size: 14px; +} +h6 { + font-size: 12px; +} + +/* plain paragraph text */ +.paragraph { +/* font-family: sans-serif; */ + margin: 0 0 10px; +} +p { +/* font-family: sans-serif; */ + margin: 0 0 10px; +} + +/* blockquote text */ +.quoteblock { + font-style: italic; +} +blockquote { + padding: 10px 20px; + margin: 0 0 20px; + font-size: 17.5px; + border-left: 5px solid #eeeeee; +} +blockquote p:last-child, +blockquote ul:last-child, +blockquote ol:last-child { + margin-bottom: 0; +} +.blockquote-reverse, +blockquote.pull-right { + padding-right: 15px; + padding-left: 0; + border-right: 5px solid #eeeeee; + border-left: 0; + text-align: right; +} +.blockquote-reverse footer:before, +blockquote.pull-right footer:before, +.blockquote-reverse small:before, +blockquote.pull-right small:before, +.blockquote-reverse .small:before, +blockquote.pull-right .small:before { + content: ''; +} +.blockquote-reverse footer:after, +blockquote.pull-right footer:after, +.blockquote-reverse small:after, +blockquote.pull-right small:after, +.blockquote-reverse .small:after, +blockquote.pull-right .small:after { + content: '\00A0 \2014'; +} + +/* blockquote attribution text */ +.attribution, +.cite, +blockquote footer, +blockquote small, +blockquote .small { + display: block; + font-size: 80%; + line-height: 1.42857143; + color: #999999; +} +.attribution:before, +blockquote footer:before, +blockquote small:before, +blockquote .small:before { + content: '\2014 \00A0'; +} + +/* unordered list */ +ul, ol { + margin-top: 0; + margin-bottom: 10px; +} +ul ul, +ol ul, +ul ol, +ol ol { + margin-bottom: 0; +} + + +/* links */ +a { + color: #158cba; + background-color: transparent; + text-decoration: none; +} +a:hover, +a:focus { + color: #158cba; + text-decoration: underline; +} +a:focus { + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +/* horizontal rules */ +hr { + margin-top: 20px; + margin-bottom: 20px; + border: 0; + border-top: 1px solid #eeeeee; +} + +/* table */ +table { + background-color: transparent; + width: 100%; + max-width: 100%; + margin-bottom: 20px; + border-collapse: collapse; +} +table col[class*="col-"] { + position: static; + float: none; + display: table-column; +} +table td[class*="col-"], +table th[class*="col-"] { + position: static; + float: none; + display: table-cell; +} + +/* table caption */ +caption { + padding-top: 8px; + padding-bottom: 8px; + color: #999999; + text-align: left; +} + +/* table header row */ +thead { + border-bottom: 2px solid #eeeeee; +} + +/* table header cell */ +th { + text-align: left; + padding-left: 8px; +} + +/* table footer */ +tfoot { + color: #807F81; + border-top: 1px solid #eeeeee; +} + +/* table cell */ +td { + border-top: 1px solid #dddddd; +} +td p { + margin: auto; + padding: 8px; +} + +/* table body */ +tbody > tr:nth-of-type(odd) { + background-color: #f9f9f9; +} +tbody > tr:hover { + background-color: #f5f5f5; +} + +/* inline code */ +code, +kbd, +pre, +samp { + font-family: monospace, monospace; + font-size: 1em; +} +code { + padding: 2px 4px; + font-size: 90%; + color: #333333; + background-color: #f5f5f5; + border-radius: 4px; +} +kbd { + padding: 2px 4px; + font-size: 90%; + color: #ffffff; + background-color: #333333; + border-radius: 2px; + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25); +} +kbd kbd { + padding: 0; + font-size: 100%; + font-weight: bold; + -webkit-box-shadow: none; + box-shadow: none; +} +pre { + display: block; + padding: 9.5px; + margin: 0 0 10px; + font-size: 13px; + line-height: 1.42857143; + word-break: break-all; + word-wrap: break-word; + color: #333333; + background-color: #f5f5f5; + border: 1px solid #cccccc; + border-radius: 4px; +} +pre code { + padding: 0; + font-size: inherit; + color: inherit; + white-space: pre-wrap; + background-color: transparent; + border-radius: 0; +} + +/* image */ +img { + max-width: 100%; + vertical-align: middle; +} + +/* footer section */ +#footer { + margin-top: 22px; + padding: 14px 16px; + color: #555555; + background-color: #f8f8f8; +} + +/* responsiveness fixes */ +video { + max-width: 100%; +} + +/* table of Contents sidebar */ +#toctitle { + color: #ffffff; +} + +#toc ul { + display: inline; + list-style-type: none; + margin: 0; + padding: 0; + overflow: hidden; +} + +#toc li { + display: block; +} + +#toc a { + background-color: #f8f8f8; + float: left; + color: #555555; + text-align: center; + padding: 14px 16px; + text-decoration: none; +} + +#toc li a:hover { + background-color: #eeeeee; + text-decoration: none; +} + +#toc:after { + content: " "; + visibility: hidden; + display: block; + height: 0; + clear: both; +} + +@media all and (max-width: 600px) { +table { + width: 55vw!important; + font-size: 3vw; +}} diff --git a/docs/stylesheets/boot-paper.css b/docs/stylesheets/boot-paper.css new file mode 100644 index 000000000000..2de9c1e46d9f --- /dev/null +++ b/docs/stylesheets/boot-paper.css @@ -0,0 +1,350 @@ +/* Based on Paper from Bootswatch (http://bootswatch.com/paper/) */ +@import url("https://fonts.googleapis.com/css?family=Roboto:300,400,500,700"); + +/* document body (contains all content) */ +body { + font-family: "Roboto", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 13px; + line-height: 1.846; + color: #666666; + background-color: #ffffff; + margin-left: 10%; + margin-right: 10%; +} + +/* document header (contains title etc) */ +#header { + width: 100%; +} +#header>h1 { + border-bottom: 1px solid #ddddd8; + padding-bottom: 8px; +} + +/* headings */ +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: inherit; + font-weight: 400; + line-height: 1.1; + color: #444444; +} +h1, +h2, +h3 { + margin-top: 23px; + margin-bottom: 11.5px; +} +h4, +h5, +h6 { + margin-top: 11.5px; + margin-bottom: 11.5px; +} +h1 { +/* font-size: 4em; */ + font-size: 56px; +} +h2 { + font-size: 45px; +} +h3 { + font-size: 34px; +} +h4 { + font-size: 24px; +} +h5 { + font-size: 20px; +} +h6 { + font-size: 14px; +} + +/* plain paragraph text */ +.paragraph { +/* font-family: sans-serif; */ + margin: 0 0 11.5px; +} +p { +/* font-family: sans-serif; */ + margin: 0 0 11.5px; +} + +/* blockquote text */ +.quoteblock { + font-style: italic; +} +blockquote { + padding: 11.5px 23px; + margin: 0 0 23px; + font-size: 16.25px; + border-left: 5px solid #eeeeee; +} +blockquote p:last-child, +blockquote ul:last-child, +blockquote ol:last-child { + margin-bottom: 0; +} +.blockquote-reverse, +blockquote.pull-right { + padding-right: 15px; + padding-left: 0; + border-right: 5px solid #eeeeee; + border-left: 0; + text-align: right; +} +.blockquote-reverse footer:before, +blockquote.pull-right footer:before, +.blockquote-reverse small:before, +blockquote.pull-right small:before, +.blockquote-reverse .small:before, +blockquote.pull-right .small:before { + content: ''; +} +.blockquote-reverse footer:after, +blockquote.pull-right footer:after, +.blockquote-reverse small:after, +blockquote.pull-right small:after, +.blockquote-reverse .small:after, +blockquote.pull-right .small:after { + content: '\00A0 \2014'; +} + +/* blockquote attribution text */ +.attribution, +.cite, +blockquote footer, +blockquote small, +blockquote .small { + display: block; +/* font-size: 80%; */ + line-height: 1.846; + color: #bbbbbb; +} +.attribution:before, +blockquote footer:before, +blockquote small:before, +blockquote .small:before { + content: '\2014 \00A0'; +} + +/* unordered list */ +ul, ol { + margin-top: 0; + margin-bottom: 11.5px; +} +ul ul, +ol ul, +ul ol, +ol ol { + margin-bottom: 0; +} + + +/* links */ +a { + color: #2196f3; + text-decoration: none; +} +a:hover, +a:focus { + color: #0a6ebd; + text-decoration: underline; +} +a:focus { + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +/* horizontal rules */ +hr { + margin-top: 23px; + margin-bottom: 23px; + border: 0; + border-top: 1px solid #eeeeee; +} + +/* table */ +table { + background-color: transparent; + width: 100%; + max-width: 100%; + margin-bottom: 20px; + border-collapse: collapse; +} +table col[class*="col-"] { + position: static; + float: none; + display: table-column; +} +table td[class*="col-"], +table th[class*="col-"] { + position: static; + float: none; + display: table-cell; +} + +/* table caption */ +caption { + padding-top: 8px; + padding-bottom: 8px; + color: #bbbbbb; + text-align: left; +} + +/* table header row */ +thead { + border-bottom: 2px solid #dddddd; +} + +/* table header cell */ +th { + text-align: left; + padding-left: 8px; +} + +/* table footer */ +tfoot { + color: #807F81; + border-top: 1px solid #dddddd; +} + +/* table cell */ +td { + border-top: 1px solid #dddddd; +} +td p { + margin: auto; + padding: 8px; +} + +/* table body */ +tbody > tr:nth-of-type(odd) { + background-color: #f9f9f9; +} +tbody > tr:hover { + background-color: #f5f5f5; +} + +/* inline code */ +code, +kbd, +pre, +samp { + font-family: monospace, monospace; + font-size: 1em; +} +code { + padding: 2px 4px; + font-size: 90%; + color: #212121; + background-color: #f5f5f5; + border-radius: 3px; +} +kbd { + padding: 2px 4px; + font-size: 90%; + color: #ffffff; + background-color: #333333; + border-radius: 3px; + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25); +} +kbd kbd { + padding: 0; + font-size: 100%; + font-weight: bold; + -webkit-box-shadow: none; + box-shadow: none; +} +pre { + display: block; + padding: 11px; + margin: 0 0 11.5px; + font-size: 12px; + line-height: 1.846; + word-break: break-all; + word-wrap: break-word; + color: #212121; + background-color: #f5f5f5; + border: 1px solid #cccccc; + border-radius: 3px; +} +pre code { + padding: 0; + font-size: inherit; + color: inherit; + white-space: pre-wrap; + background-color: transparent; + border-radius: 0; +} + +/* image */ +img { + max-width: 100%; + vertical-align: middle; +} + +/* footer section */ +#footer { + margin-top: 22px; + padding: 14px 16px; + color: #666666; + background-color: #ffffff; +} + +/* responsiveness fixes */ +video { + max-width: 100%; +} + +/* table of Contents sidebar */ +#toctitle { + color: #ffffff; +} + +#toc ul { + display: inline; + list-style-type: none; + margin: 0; + padding: 0; + overflow: hidden; +} + +#toc li { + display: block; +} + +#toc a { + background-color: #ffffff; + float: left; + color: #666666; + text-align: center; + padding: 14px 16px; + text-decoration: none; +} + +#toc li a:hover { + color: #212121; + background-color: #ffffff; + text-decoration: none; +} + +#toc:after { + content: " "; + visibility: hidden; + display: block; + height: 0; + clear: both; +} + +@media all and (max-width: 600px) { +table { + width: 55vw!important; + font-size: 3vw; +}} diff --git a/docs/stylesheets/boot-readable.css b/docs/stylesheets/boot-readable.css new file mode 100644 index 000000000000..3de52ef6c1f8 --- /dev/null +++ b/docs/stylesheets/boot-readable.css @@ -0,0 +1,350 @@ +/* Based on Readable from Bootswatch (http://bootswatch.com/readable/) */ +@import url("https://fonts.googleapis.com/css?family=Raleway:400,700"); + +/* document body (contains all content) */ +body { + font-family: Georgia, "Times New Roman", Times, serif; + font-size: 16px; + line-height: 1.42857143; + color: #333333; + background-color: #ffffff; + margin-left: 10%; + margin-right: 10%; +} + +/* document header (contains title etc) */ +#header { + width: 100%; +} +#header>h1 { + border-bottom: 1px solid #ddddd8; + padding-bottom: 8px; +} + + +/* headings */ +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: "Raleway", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: bold; + line-height: 1.1; + color: inherit; +} +h1, +h2, +h3 { + margin-top: 22px; + margin-bottom: 11px; +} +h4, +h5, +h6 { + margin-top: 11px; + margin-bottom: 11px; +} +h1 { + font-size: 4em; +/* font-size: 41px; */ +} +h2 { + font-size: 34px; +} +h3 { + font-size: 28px; +} +h4 { + font-size: 20px; +} +h5 { + font-size: 16px; +} +h6 { + font-size: 14px; +} + +/* plain paragraph text */ +.paragraph { +/* font-family: sans-serif; */ + margin: 0 0 11px; +} +p { +/* font-family: sans-serif; */ + margin: 0 0 11px; +} + +/* blockquote text */ +.quoteblock { + font-style: italic; +} +blockquote { + padding: 11px 22px; + margin: 0 0 22px; + font-size: 20px; + border-left: 5px solid #4582ec; +} +blockquote p:last-child, +blockquote ul:last-child, +blockquote ol:last-child { + margin-bottom: 0; +} +.blockquote-reverse, +blockquote.pull-right { + padding-right: 15px; + padding-left: 0; + border-right: 5px solid #4582ec; + border-left: 0; + text-align: right; +} +.blockquote-reverse footer:before, +blockquote.pull-right footer:before, +.blockquote-reverse small:before, +blockquote.pull-right small:before, +.blockquote-reverse .small:before, +blockquote.pull-right .small:before { + content: ''; +} +.blockquote-reverse footer:after, +blockquote.pull-right footer:after, +.blockquote-reverse small:after, +blockquote.pull-right small:after, +.blockquote-reverse .small:after, +blockquote.pull-right .small:after { + content: '\00A0 \2014'; +} + +/* blockquote attribution text */ +.attribution, +.cite, +blockquote footer, +blockquote small, +blockquote .small { + display: block; + font-size: 80%; + line-height: 1.42857143; + color: #333333; +} +.attribution:before, +blockquote footer:before, +blockquote small:before, +blockquote .small:before { + content: '\2014 \00A0'; +} + +/* unordered list */ +ul, ol { + margin-top: 0; + margin-bottom: 11px; +} +ul ul, +ol ul, +ul ol, +ol ol { + margin-bottom: 0; +} + + +/* links */ +a { + color: #4582ec; + background-color: transparent; + text-decoration: none; +} +a:hover, +a:focus { + color: #134fb8; + text-decoration: underline; +} +a:focus { + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +/* horizontal rules */ +hr { + margin-top: 22px; + margin-bottom: 22px; + border: 0; + border-top: 1px solid #eeeeee; +} + +/* table */ +table { + background-color: transparent; + width: 100%; + max-width: 100%; + margin-bottom: 22px; + border-collapse: collapse; +} +table col[class*="col-"] { + position: static; + float: none; + display: table-column; +} +table td[class*="col-"], +table th[class*="col-"] { + position: static; + float: none; + display: table-cell; +} + +/* table caption */ +caption { + padding-top: 8px; + padding-bottom: 8px; + color: #b3b3b3; + text-align: left; +} + +/* table header row */ +thead { + border-bottom: 2px solid #dddddd; +} + +/* table header cell */ +th { + text-align: left; + padding-left: 8px; +} + +/* table footer */ +tfoot { + color: #807F81; + border-top: 1px solid #dddddd; +} + +/* table cell */ +td { + border-top: 1px solid #dddddd; +} +td p { + margin: auto; + padding: 8px; +} + +/* table body */ +tbody > tr:nth-of-type(odd) { + background-color: #f9f9f9; +} +tbody > tr:hover { + background-color: #f5f5f5; +} + +/* inline code */ +code, +kbd, +pre, +samp { + font-family: Menlo, Monaco, Consolas, "Courier New", monospace; +} +code { + padding: 2px 4px; + font-size: 90%; + color: #c7254e; + background-color: #f9f2f4; + border-radius: 4px; +} +kbd { + padding: 2px 4px; + font-size: 90%; + color: #ffffff; + background-color: #333333; + border-radius: 0; + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25); +} +kbd kbd { + padding: 0; + font-size: 100%; + font-weight: bold; + -webkit-box-shadow: none; + box-shadow: none; +} +pre { + display: block; + padding: 10.5px; + margin: 0 0 11px; + font-size: 15px; + line-height: 1.42857143; + word-break: break-all; + word-wrap: break-word; + color: #333333; + background-color: #f5f5f5; + border: 1px solid #cccccc; + border-radius: 4px; +} +pre code { + padding: 0; + font-size: inherit; + color: inherit; + white-space: pre-wrap; + background-color: transparent; + border-radius: 0; +} + +/* image */ +img { + max-width: 100%; + vertical-align: middle; +} + +/* footer section */ +#footer { + margin-top: 22px; + padding: 14px 16px; + color: white; + background-color: #4582ec; +} + +/* responsiveness fixes */ +video { + max-width: 100%; +} + +/* table of Contents sidebar */ +#toctitle { + color: #ffffff; +} + +#toc ul { + display: inline; + list-style-type: none; + margin: 0; + padding: 0; + overflow: hidden; +} + +#toc li { + display: block; +} + +#toc a { + background-color: #4582ec; + float: left; + color: white; + text-align: center; + padding: 14px 16px; + text-decoration: none; +} + +#toc li a:hover { + background-color: #1863e6; + text-decoration: none; +} + +#toc:after { + content: " "; + visibility: hidden; + display: block; + height: 0; + clear: both; +} + +@media all and (max-width: 600px) { +table { + width: 55vw!important; + font-size: 3vw; +}} diff --git a/docs/stylesheets/boot-sandstone.css b/docs/stylesheets/boot-sandstone.css new file mode 100644 index 000000000000..ee3cb80a6f94 --- /dev/null +++ b/docs/stylesheets/boot-sandstone.css @@ -0,0 +1,352 @@ +/* Based on Sandstone from Bootswatch (http://bootswatch.com/sandstone/) */ +@import url("https://fonts.googleapis.com/css?family=Roboto:400,500,700"); + +/* document body (contains all content) */ +body { + font-family: "Roboto", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 14px; + line-height: 1.42857143; + color: #3e3f3a; + background-color: #ffffff; + margin-left: 10%; + margin-right: 10%; +} + +/* document header (contains title etc) */ +#header { + width: 100%; +} +#header>h1 { + border-bottom: 1px solid #ddddd8; + padding-bottom: 8px; +} + + +/* headings */ +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: inherit; + font-weight: 400; + line-height: 1.1; + color: inherit; +} +h1, +h2, +h3 { + margin-top: 20px; + margin-bottom: 10px; +} +h4, +h5, +h6 { + margin-top: 10px; + margin-bottom: 10px; +} +h1 { + font-size: 4em; +/* font-size: 36px; */ +} +h2 { + font-size: 30px; +} +h3 { + font-size: 24px; +} +h4 { + font-size: 18px; +} +h5 { + font-size: 14px; +} +h6 { + font-size: 12px; +} + +/* plain paragraph text */ +.paragraph { + margin: 0 0 10px; +} +p { + margin: 0 0 10px; +} + +/* blockquote text */ +.quoteblock { + font-style: italic; +} +blockquote { + padding: 10px 20px; + margin: 0 0 20px; + font-size: 17.5px; + border-left: 5px solid #dfd7ca; +} +blockquote p:last-child, +blockquote ul:last-child, +blockquote ol:last-child { + margin-bottom: 0; +} +.blockquote-reverse, +blockquote.pull-right { + padding-right: 15px; + padding-left: 0; + border-right: 5px solid #dfd7ca; + border-left: 0; + text-align: right; +} +.blockquote-reverse footer:before, +blockquote.pull-right footer:before, +.blockquote-reverse small:before, +blockquote.pull-right small:before, +.blockquote-reverse .small:before, +blockquote.pull-right .small:before { + content: ''; +} +.blockquote-reverse footer:after, +blockquote.pull-right footer:after, +.blockquote-reverse small:after, +blockquote.pull-right small:after, +.blockquote-reverse .small:after, +blockquote.pull-right .small:after { + content: '\00A0 \2014'; +} + +/* blockquote attribution text */ +.attribution, +.cite, +blockquote footer, +blockquote small, +blockquote .small { + display: block; + font-size: 80%; + line-height: 1.42857143; + color: #3e3f3a; +} +.attribution:before, +blockquote footer:before, +blockquote small:before, +blockquote .small:before { + content: '\2014 \00A0'; +} + +/* unordered list */ +ul, ol { + margin-top: 0; + margin-bottom: 10px; +} +ul ul, +ol ul, +ul ol, +ol ol { + margin-bottom: 0; +} + + +/* links */ +a { + color: #93c54b; + text-decoration: none; +} +a:hover, +a:focus { + color: #79a736; + text-decoration: underline; +} +a:focus { + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +/* horizontal rules */ +hr { + margin-top: 20px; + margin-bottom: 20px; + border: 0; + border-top: 1px solid #f8f5f0; +} + +/* table */ +table { + background-color: transparent; + width: 100%; + max-width: 100%; + margin-bottom: 20px; + border-collapse: collapse; +} +table col[class*="col-"] { + position: static; + float: none; + display: table-column; +} +table td[class*="col-"], +table th[class*="col-"] { + position: static; + float: none; + display: table-cell; +} + +/* table caption */ +caption { + padding-top: 8px; + padding-bottom: 8px; + color: #98978b; + text-align: left; +} + +/* table header row */ +thead { + border-bottom: 2px solid #dfd7ca; +} + +/* table header cell */ +th { + text-align: left; + padding-left: 8px; +} + +/* table footer */ +tfoot { + color: #807F81; + border-top: 1px solid #dfd7ca; +} + +/* table cell */ +td { + border-top: 1px solid #dfd7ca; +} +td p { + margin: auto; + padding: 8px; +} + +/* table body */ +tbody > tr:nth-of-type(odd) { + background-color: #f8f5f0; +} +tbody > tr:hover { + background-color: #f8f5f0; +} + +/* inline code */ +code, +kbd, +pre, +samp { + font-family: monospace, monospace; + font-size: 1em; +} +code { + padding: 2px 4px; + font-size: 90%; + color: #8e8c84; + background-color: #f5f5f5; + border-radius: 4px; +} +kbd { + padding: 2px 4px; + font-size: 90%; + color: #ffffff; + background-color: #333333; + border-radius: 3px; + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25); +} +kbd kbd { + padding: 0; + font-size: 100%; + font-weight: bold; + -webkit-box-shadow: none; + box-shadow: none; +} +pre { + display: block; + padding: 9.5px; + margin: 0 0 10px; + font-size: 13px; + line-height: 1.42857143; + word-break: break-all; + word-wrap: break-word; + color: #8e8c84; + background-color: #f5f5f5; + border: 1px solid #cccccc; + border-radius: 4px; +} +pre code { + padding: 0; + font-size: inherit; + color: inherit; + white-space: pre-wrap; + background-color: transparent; + border-radius: 0; +} + +/* image */ +img { + max-width: 100%; + vertical-align: middle; +} + +/* footer section */ +#footer { + margin-top: 22px; + padding: 14px 16px; + color: #98978b; + background-color: #3e3f3a; +} + +/* responsiveness fixes */ +video { + max-width: 100%; +} + +/* table of Contents sidebar */ +#toctitle { + display: none; +} + +#toc { + background-color: #3e3f3a; + border-radius: 4px; +} + +#toc ul { + display: inline; + list-style-type: none; + margin: 0; + padding: 0; + overflow: hidden; +} + +#toc li { + display: block; +} + +#toc a { + float: left; + color: #98978b; + text-align: center; + padding: 14px 16px; + text-decoration: none; +} + +#toc li a:hover { + color: #ffffff; + text-decoration: none; +} + +#toc:after { + content: " "; + visibility: hidden; + display: block; + height: 0; + clear: both; +} + +@media all and (max-width: 600px) { +table { + width: 55vw!important; + font-size: 3vw; +}} diff --git a/docs/stylesheets/boot-slate.css b/docs/stylesheets/boot-slate.css new file mode 100644 index 000000000000..83aa26c290ad --- /dev/null +++ b/docs/stylesheets/boot-slate.css @@ -0,0 +1,345 @@ +/* Based on Slate from Bootswatch (http://bootswatch.com/slate/) */ + +/* document body (contains all content) */ +body { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 14px; + line-height: 1.42857143; + color: #c8c8c8; + background-color: #272b30; + margin-left: 10%; + margin-right: 10%; +} + +/* document header (contains title etc) */ +#header { + width: 100%; +} +#header>h1 { + border-bottom: 1px solid #ddddd8; + padding-bottom: 8px; +} + +/* headings */ +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 500; + line-height: 1.1; + color: inherit; +} +h1, +h2, +h3 { + margin-top: 20px; + margin-bottom: 10px; +} +h4, +h5, +h6 { + margin-top: 10px; + margin-bottom: 10px; +} +h1 { + font-size: 4em; +/* font-size: 36px; */ +} +h2 { + font-size: 30px; +} +h3 { + font-size: 24px; +} +h4 { + font-size: 18px; +} +h5 { + font-size: 14px; +} +h6 { + font-size: 12px; +} + +/* plain paragraph text */ +.paragraph { +/* font-family: sans-serif; */ + margin: 0 0 10px; +} +p { +/* font-family: sans-serif; */ + margin: 0 0 10px; +} + +/* blockquote text */ +.quoteblock { + font-style: italic; +} +blockquote { + padding: 10px 20px; + margin: 0 0 20px; + font-size: 17.5px; + border-left: 5px solid #7a8288; +} +blockquote p:last-child, +blockquote ul:last-child, +blockquote ol:last-child { + margin-bottom: 0; +} +.blockquote-reverse, +blockquote.pull-right { + padding-right: 15px; + padding-left: 0; + border-right: 5px solid #7a8288; + border-left: 0; + text-align: right; +} +.blockquote-reverse footer:before, +blockquote.pull-right footer:before, +.blockquote-reverse small:before, +blockquote.pull-right small:before, +.blockquote-reverse .small:before, +blockquote.pull-right .small:before { + content: ''; +} +.blockquote-reverse footer:after, +blockquote.pull-right footer:after, +.blockquote-reverse small:after, +blockquote.pull-right small:after, +.blockquote-reverse .small:after, +blockquote.pull-right .small:after { + content: '\00A0 \2014'; +} + +/* blockquote attribution text */ +.attribution, +.cite, +blockquote footer, +blockquote small, +blockquote .small { + display: block; + line-height: 1.42857143; + color: #7a8288; +} +.attribution:before, +blockquote footer:before, +blockquote small:before, +blockquote .small:before { + content: '\2014 \00A0'; +} + +/* unordered list */ +ul, ol { + margin-top: 0; + margin-bottom: 10px; +} +ul ul, +ol ul, +ul ol, +ol ol { + margin-bottom: 0; +} + + +/* links */ +a { + color: #ffffff; + text-decoration: none; +} +a:hover, +a:focus { + color: #ffffff; + text-decoration: underline; +} +a:focus { + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +/* horizontal rules */ +hr { + margin-top: 20px; + margin-bottom: 20px; + border: 0; + border-top: 1px solid #1c1e22; +} + +/* table */ +table { + background-color: transparent; + width: 100%; + max-width: 100%; + margin-bottom: 21px; + border-collapse: collapse; +} +table col[class*="col-"] { + position: static; + float: none; + display: table-column; +} +table td[class*="col-"], +table th[class*="col-"] { + position: static; + float: none; + display: table-cell; +} + +/* table caption */ +caption { + padding-top: 8px; + padding-bottom: 8px; + color: #7a8288; + text-align: left; +} + +/* table header row */ +thead { + border-bottom: 2px solid #1c1e22; +} + +/* table header cell */ +th { + text-align: left; + padding-left: 8px; +} + +/* table footer */ +tfoot { + color: #807F81; + border-top: 1px solid #1c1e22; +} + +/* table cell */ +td { + border-top: 1px solid #1c1e22; +} +td p { + margin: auto; + padding: 8px; +} + +/* table body */ +tbody > tr:nth-of-type(odd) { + background-color: #353a41; +} +tbody > tr:hover { + background-color: #49515a; +} + +/* inline code */ +code, +kbd, +pre, +samp { + font-family: monospace, monospace; + font-size: 1em; +} +code { + padding: 2px 4px; + font-size: 90%; + color: #3a3f44; + background-color: #f5f5f5; + border-radius: 4px; +} +kbd { + padding: 2px 4px; + font-size: 90%; + color: #ffffff; + background-color: #333333; + border-radius: 3px; + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25); +} +kbd kbd { + padding: 0; + font-size: 100%; + font-weight: bold; + -webkit-box-shadow: none; + box-shadow: none; +} +pre { + display: block; + padding: 9.5px; + margin: 0 0 10px; + font-size: 13px; + line-height: 1.42857143; + word-break: break-all; + word-wrap: break-word; + color: #3a3f44; + background-color: #f5f5f5; + border: 1px solid #cccccc; + border-radius: 4px; +} +pre code { + padding: 0; + font-size: inherit; + color: inherit; + white-space: pre-wrap; + background-color: transparent; + border-radius: 0; +} + +/* image */ +img { + max-width: 100%; + vertical-align: middle; +} + +/* footer section */ +#footer { + margin-top: 22px; + padding: 14px 16px; + background-color: #3a3f44; +} + +/* responsiveness fixes */ +video { + max-width: 100%; +} + +/* table of Contents sidebar */ +#toctitle { + color: #272b30; +} + +#toc ul { + display: inline; + list-style-type: none; + margin: 0; + padding: 0; + overflow: hidden; +} + +#toc li { + display: block; +} + +#toc a { + background-color: #3a3f44; + float: left; + text-align: center; + padding: 14px 16px; + text-decoration: none; +} + +#toc li a:hover { + background-color: #272b2e; + text-decoration: none; +} + +#toc:after { + content: " "; + visibility: hidden; + display: block; + height: 0; + clear: both; +} + +@media all and (max-width: 600px) { +table { + width: 55vw!important; + font-size: 3vw; +}} diff --git a/docs/stylesheets/boot-spacelab.css b/docs/stylesheets/boot-spacelab.css new file mode 100644 index 000000000000..4f9cafc6cbba --- /dev/null +++ b/docs/stylesheets/boot-spacelab.css @@ -0,0 +1,360 @@ +/* Based on Cerulean from Bootswatch (http://bootswatch.com/cerulean/) */ +@import url("https://fonts.googleapis.com/css?family=Open+Sans:400italic,700italic,400,700"); + +/* document body (contains all content) */ +body { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 14px; + line-height: 1.42857143; + color: #666666; + background-color: #ffffff; + margin-left: 10%; + margin-right: 10%; +} + +/* document header (contains title etc) */ +#header { + width: 100%; +} +#header>h1 { + border-bottom: 1px solid #ddddd8; + padding-bottom: 8px; +} + +/* headings */ +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 500; + line-height: 1.1; + color: #2d2d2d; +} +h1, +h2, +h3 { + margin-top: 20px; + margin-bottom: 10px; +} +h4, +h5, +h6 { + margin-top: 10px; + margin-bottom: 10px; +} +h1 { + font-size: 4em; +/* font-size: 36px; */ +} +h2 { + font-size: 30px; +} +h3 { + font-size: 24px; +} +h4 { + font-size: 18px; +} +h5 { + font-size: 14px; +} +h6 { + font-size: 12px; +} + +/* plain paragraph text */ +.paragraph { +/* font-family: sans-serif; */ + margin: 0 0 10px; +} +p { + margin: 0 0 10px; +} + +/* blockquote text */ +.quoteblock { + font-style: italic; +} +blockquote { + padding: 10px 20px; + margin: 0 0 20px; + font-size: 17.5px; + border-left: 5px solid #eeeeee; +} +blockquote p:last-child, +blockquote ul:last-child, +blockquote ol:last-child { + margin-bottom: 0; +} +.blockquote-reverse, +blockquote.pull-right { + padding-right: 15px; + padding-left: 0; + border-right: 5px solid #eeeeee; + border-left: 0; + text-align: right; +} +.blockquote-reverse footer:before, +blockquote.pull-right footer:before, +.blockquote-reverse small:before, +blockquote.pull-right small:before, +.blockquote-reverse .small:before, +blockquote.pull-right .small:before { + content: ''; +} +.blockquote-reverse footer:after, +blockquote.pull-right footer:after, +.blockquote-reverse small:after, +blockquote.pull-right small:after, +.blockquote-reverse .small:after, +blockquote.pull-right .small:after { + content: '\00A0 \2014'; +} + +/* blockquote attribution text */ +.attribution, +.cite, +blockquote footer, +blockquote small, +blockquote .small { + display: block; + line-height: 1.42857143; + color: #999999; +} +.attribution:before, +blockquote footer:before, +blockquote small:before, +blockquote .small:before { + content: '\2014 \00A0'; +} + +/* unordered list */ +ul, ol { + margin-top: 0; + margin-bottom: 10px; +} +ul ul, +ol ul, +ul ol, +ol ol { + margin-bottom: 0; +} + + +/* links */ +a { + color: #3399f3; + text-decoration: none; +} +a:hover, +a:focus { + color: #3399f3; + text-decoration: underline; +} +a:focus { + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +/* horizontal rules */ +hr { + margin-top: 20px; + margin-bottom: 20px; + border: 0; + border-top: 1px solid #eeeeee; +} + +/* table */ +table { + background-color: transparent; + width: 100%; + max-width: 100%; + margin-bottom: 21px; + border-collapse: collapse; +} +table col[class*="col-"] { + position: static; + float: none; + display: table-column; +} +table td[class*="col-"], +table th[class*="col-"] { + position: static; + float: none; + display: table-cell; +} + +/* table caption */ +caption { + padding-top: 8px; + padding-bottom: 8px; + color: #999999; + text-align: left; +} + +/* table header row */ +thead { + border-bottom: 2px solid #dddddd; +} + +/* table header cell */ +th { + text-align: left; + padding-left: 8px; +} + +/* table footer */ +tfoot { + color: #807F81; + border-top: 1px solid #dddddd; +} + +/* table cell */ +td { + border-top: 1px solid #dddddd; +} +td p { + margin: auto; + padding: 8px; +} + +/* table body */ +tbody > tr:nth-of-type(odd) { + background-color: #f9f9f9; +} +tbody > tr:hover { + background-color: #f5f5f5; +} + +/* inline code */ +code, +kbd, +pre, +samp { + font-family: monospace, monospace; + font-size: 1em; +} +code { + padding: 2px 4px; + font-size: 90%; + color: #333333; + background-color: #f5f5f5; + border-radius: 4px; +} +kbd { + padding: 2px 4px; + font-size: 90%; + color: #ffffff; + background-color: #333333; + border-radius: 3px; + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25); +} +kbd kbd { + padding: 0; + font-size: 100%; + font-weight: bold; + -webkit-box-shadow: none; + box-shadow: none; +} +pre { + display: block; + padding: 9.5px; + margin: 0 0 10px; + font-size: 13px; + line-height: 1.42857143; + word-break: break-all; + word-wrap: break-word; + color: #333333; + background-color: #f5f5f5; + border: 1px solid #cccccc; + border-radius: 4px; +} +pre code { + padding: 0; + font-size: inherit; + color: inherit; + white-space: pre-wrap; + background-color: transparent; + border-radius: 0; +} + +/* image */ +img { + max-width: 100%; + vertical-align: middle; +} + +/* footer section */ +#footer { + margin-top: 22px; + padding: 14px 16px; + color: #777777; + background-color: #eeeeee; +} + +/* responsiveness fixes */ +video { + max-width: 100%; +} + +/* table of Contents sidebar */ +#toctitle { + display: none; +} + +#toc { + margin-top: 20px; + background-image: -webkit-linear-gradient(#ffffff, #eeeeee 50%, #e4e4e4); + background-image: -o-linear-gradient(#ffffff, #eeeeee 50%, #e4e4e4); + background-image: -webkit-gradient(linear, left top, left bottom, from(#ffffff), color-stop(50%, #eeeeee), to(#e4e4e4)); + background-image: linear-gradient(#ffffff, #eeeeee 50%, #e4e4e4); + background-repeat: no-repeat; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe4e4e4', GradientType=0); + -webkit-filter: none; + filter: none; + border: 1px solid #d5d5d5; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.3); + border-radius: 4px 4px; +} + +#toc ul { + display: inline; + list-style-type: none; + margin: 0; + padding: 0; + overflow: hidden; +} + +#toc li { + display: block; +} + +#toc a { + color: #777777; + float: left; + text-align: center; + padding: 14px 16px; + text-decoration: none; +} + +#toc li a:hover { + color: #3399f3; +} + +#toc:after { + content: " "; + visibility: hidden; + display: block; + height: 0; + clear: both; +} + +@media all and (max-width: 600px) { +table { + width: 55vw!important; + font-size: 3vw; +}} diff --git a/docs/stylesheets/boot-superhero.css b/docs/stylesheets/boot-superhero.css new file mode 100644 index 000000000000..84f2ec27992f --- /dev/null +++ b/docs/stylesheets/boot-superhero.css @@ -0,0 +1,347 @@ +/* Based on Superhero from Bootswatch (http://bootswatch.com/superhero/) */ +@import url("https://fonts.googleapis.com/css?family=Lato:300,400,700"); + +/* document body (contains all content) */ +body { + font-family: "Lato", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 15px; + line-height: 1.42857143; + color: #ebebeb; + background-color: #2b3e50; + margin-left: 10%; + margin-right: 10%; +} + +/* document header (contains title etc) */ +#header { + width: 100%; +} +#header>h1 { + border-bottom: 1px solid #ddddd8; + padding-bottom: 8px; +} + +/* headings */ +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: inherit; + font-weight: 400; + line-height: 1.1; + color: inherit; +} +h1, +h2, +h3 { + margin-top: 21px; + margin-bottom: 10.5px; +} +h4, +h5, +h6 { + margin-top: 10.5px; + margin-bottom: 10.5px; +} +h1 { + font-size: 4em; +/* font-size: 39px; */ +} +h2 { + font-size: 32px; +} +h3 { + font-size: 26px; +} +h4 { + font-size: 19px; +} +h5 { + font-size: 15px; +} +h6 { + font-size: 13px; +} + +/* plain paragraph text */ +.paragraph { +/* font-family: sans-serif; */ + margin: 0 0 10.5px; +} +p { +/* font-family: sans-serif; */ + margin: 0 0 10.5px; +} + +/* blockquote text */ +.quoteblock { + font-style: italic; +} +blockquote { + padding: 10.5px 21px; + margin: 0 0 21px; + font-size: 18.75px; + border-left: 5px solid #4e5d6c; +} +blockquote p:last-child, +blockquote ul:last-child, +blockquote ol:last-child { + margin-bottom: 0; +} +.blockquote-reverse, +blockquote.pull-right { + padding-right: 15px; + padding-left: 0; + border-right: 5px solid #4e5d6c; + border-left: 0; + text-align: right; +} +.blockquote-reverse footer:before, +blockquote.pull-right footer:before, +.blockquote-reverse small:before, +blockquote.pull-right small:before, +.blockquote-reverse .small:before, +blockquote.pull-right .small:before { + content: ''; +} +.blockquote-reverse footer:after, +blockquote.pull-right footer:after, +.blockquote-reverse small:after, +blockquote.pull-right small:after, +.blockquote-reverse .small:after, +blockquote.pull-right .small:after { + content: '\00A0 \2014'; +} + +/* blockquote attribution text */ +.attribution, +.cite, +blockquote footer, +blockquote small, +blockquote .small { + display: block; + line-height: 1.42857143; + color: #ebebeb; +} +.attribution:before, +blockquote footer:before, +blockquote small:before, +blockquote .small:before { + content: '\2014 \00A0'; +} + +/* unordered list */ +ul, ol { + margin-top: 0; + margin-bottom: 10.5px; +} +ul ul, +ol ul, +ul ol, +ol ol { + margin-bottom: 0; +} + + +/* links */ +a { + color: #df691a; + text-decoration: none; +} +a:hover, +a:focus { + color: #df691a; + text-decoration: underline; +} +a:focus { + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +/* horizontal rules */ +hr { + margin-top: 21px; + margin-bottom: 21px; + border: 0; + border-top: 1px solid #596a7b; +} + +/* table */ +table { + background-color: transparent; + width: 100%; + max-width: 100%; + margin-bottom: 21px; + border-collapse: collapse; +} +table col[class*="col-"] { + position: static; + float: none; + display: table-column; +} +table td[class*="col-"], +table th[class*="col-"] { + position: static; + float: none; + display: table-cell; +} + +/* table caption */ +caption { + padding-top: 6px; + padding-bottom: 6px; + color: #4e5d6c; + text-align: left; +} + +/* table header row */ +thead { + border-bottom: 2px solid #4e5d6c; +} + +/* table header cell */ +th { + text-align: left; + padding-left: 8px; +} + +/* table footer */ +tfoot { + color: #807F81; + border-top: 1px solid #4e5d6c; +} + +/* table cell */ +td { + border-top: 1px solid #4e5d6c; +} +td p { + margin: auto; + padding: 8px; +} + +/* table body */ +tbody > tr:nth-of-type(odd) { + background-color: #4e5d6c; +} +tbody > tr:hover { + background-color: #485563; +} + +/* inline code */ +code, +kbd, +pre, +samp { + font-family: monospace, monospace; + font-size: 1em; +} +code { + padding: 2px 4px; + font-size: 90%; + color: #333333; + background-color: #f5f5f5; + border-radius: 0; +} +kbd { + padding: 2px 4px; + font-size: 90%; + color: #ffffff; + background-color: #333333; + border-radius: 0; + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25); +} +kbd kbd { + padding: 0; + font-size: 100%; + font-weight: bold; + -webkit-box-shadow: none; + box-shadow: none; +} +pre { + display: block; + padding: 10px; + margin: 0 0 10.5px; + font-size: 14px; + line-height: 1.42857143; + word-break: break-all; + word-wrap: break-word; + color: #333333; + background-color: #f5f5f5; + border: 1px solid #cccccc; + border-radius: 0; +} +pre code { + padding: 0; + font-size: inherit; + color: inherit; + white-space: pre-wrap; + background-color: transparent; + border-radius: 0; +} + +/* image */ +img { + max-width: 100%; + vertical-align: middle; +} + +/* footer section */ +#footer { + margin-top: 22px; + padding: 14px 16px; + background-color: #4e5d6c; +} + +/* responsiveness fixes */ +video { + max-width: 100%; +} + +/* table of Contents sidebar */ +#toctitle { + color: #2b3e50; +} + +#toc ul { + display: inline; + list-style-type: none; + margin: 0; + padding: 0; + overflow: hidden; +} + +#toc li { + display: block; +} + +#toc a { + color: #ebebeb; + background-color: #4e5d6c; + float: left; + text-align: center; + padding: 14px 16px; + text-decoration: none; +} + +#toc li a:hover { + background-color: #485563; + text-decoration: none; +} + +#toc:after { + content: " "; + visibility: hidden; + display: block; + height: 0; + clear: both; +} + +@media all and (max-width: 600px) { +table { + width: 55vw!important; + font-size: 3vw; +}} diff --git a/docs/stylesheets/boot-yeti.css b/docs/stylesheets/boot-yeti.css new file mode 100644 index 000000000000..51e2b66b9b55 --- /dev/null +++ b/docs/stylesheets/boot-yeti.css @@ -0,0 +1,346 @@ +/* Based on Yeti from Bootswatch (http://bootswatch.com/yeti/) */ +@import url("https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,700italic,400,300,700"); + +/* document body (contains all content) */ +body { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 15px; + line-height: 1.4; + color: #222222; + background-color: #ffffff; + margin-left: 10%; + margin-right: 10%; +} + +/* document header (contains title etc) */ +#header { + width: 100%; +} +#header>h1 { + border-bottom: 1px solid #ddddd8; + padding-bottom: 8px; +} + +/* headings */ +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; + line-height: 1.1; + color: inherit; +} +h1, +h2, +h3 { + margin-top: 21px; + margin-bottom: 10.5px; +} +h4, +h5, +h6 { + margin-top: 10.5px; + margin-bottom: 10.5px; +} +h1 { + font-size: 4em; +/* font-size: 39px; */ +} +h2 { + font-size: 32px; +} +h3 { + font-size: 26px; +} +h4 { + font-size: 19px; +} +h5 { + font-size: 15px; +} +h6 { + font-size: 13px; +} + +/* plain paragraph text */ +.paragraph { +/* font-family: sans-serif; */ + margin: 0 0 10.5px; +} +p { + margin: 0 0 10.5px; +} + +/* blockquote text */ +.quoteblock { + font-style: italic; +} +blockquote { + padding: 10.5px 21px; + margin: 0 0 21px; + font-size: 18.75px; + border-left: 5px solid #dddddd; +} +blockquote p:last-child, +blockquote ul:last-child, +blockquote ol:last-child { + margin-bottom: 0; +} +.blockquote-reverse, +blockquote.pull-right { + padding-right: 15px; + padding-left: 0; + border-right: 5px solid #dddddd; + border-left: 0; + text-align: right; +} +.blockquote-reverse footer:before, +blockquote.pull-right footer:before, +.blockquote-reverse small:before, +blockquote.pull-right small:before, +.blockquote-reverse .small:before, +blockquote.pull-right .small:before { + content: ''; +} +.blockquote-reverse footer:after, +blockquote.pull-right footer:after, +.blockquote-reverse small:after, +blockquote.pull-right small:after, +.blockquote-reverse .small:after, +blockquote.pull-right .small:after { + content: '\00A0 \2014'; +} + +/* blockquote attribution text */ +.attribution, +.cite, +blockquote footer, +blockquote small, +blockquote .small { + display: block; + line-height: 1.4; + color: #6f6f6f; +} +.attribution:before, +blockquote footer:before, +blockquote small:before, +blockquote .small:before { + content: '\2014 \00A0'; +} + +/* unordered list */ +ul, ol { + margin-top: 0; + margin-bottom: 10.5px; +} +ul ul, +ol ul, +ul ol, +ol ol { + margin-bottom: 0; +} + +/* links */ +a { + color: #008cba; + text-decoration: none; +} +a:hover, +a:focus { + color: #008cba; + text-decoration: underline; +} +a:focus { + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +/* horizontal rules */ +hr { + margin-top: 21px; + margin-bottom: 21px; + border: 0; + border-top: 1px solid #dddddd; +} + +/* table */ +table { + background-color: transparent; + width: 100%; + max-width: 100%; + margin-bottom: 21px; + border-collapse: collapse; +} +table col[class*="col-"] { + position: static; + float: none; + display: table-column; +} +table td[class*="col-"], +table th[class*="col-"] { + position: static; + float: none; + display: table-cell; +} + +/* table caption */ +caption { + padding-top: 8px; + padding-bottom: 8px; + color: #999999; + text-align: left; +} + +/* table header row */ +thead { + border-bottom: 2px solid #dddddd; +} + +/* table header cell */ +th { + text-align: left; + padding-left: 8px; +} + +/* table footer */ +tfoot { + color: #807F81; + border-top: 1px solid #dddddd; +} + +/* table cell */ +td { + border-top: 1px solid #dddddd; +} +td p { + margin: auto; + padding: 8px; +} + +/* table body */ +tbody > tr:nth-of-type(odd) { + background-color: #f9f9f9; +} +tbody > tr:hover { + background-color: #f5f5f5; +} + +/* inline code */ +code, +kbd, +pre, +samp { + font-family: monospace, monospace; + font-size: 1em; +} +code { + padding: 2px 4px; + font-size: 90%; + color: #333333; + background-color: #f5f5f5; + border-radius: 0; +} +kbd { + padding: 2px 4px; + font-size: 90%; + color: #ffffff; + background-color: #333333; + border-radius: 0; + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25); +} +kbd kbd { + padding: 0; + font-size: 100%; + font-weight: bold; + -webkit-box-shadow: none; + box-shadow: none; +} +pre { + display: block; + padding: 10px; + margin: 0 0 10.5px; + font-size: 14px; + line-height: 1.4; + word-break: break-all; + word-wrap: break-word; + color: #333333; + background-color: #f5f5f5; + border: 1px solid #cccccc; + border-radius: 0; +} +pre code { + padding: 0; + font-size: inherit; + color: inherit; + white-space: pre-wrap; + background-color: transparent; + border-radius: 0; +} + +/* image */ +img { + max-width: 100%; + vertical-align: middle; +} + +/* footer section */ +#footer { + margin-top: 22px; + padding: 14px 16px; + color: #ffffff; + background-color: #333333; +} + +/* responsiveness fixes */ +video { + max-width: 100%; +} + +/* table of Contents sidebar */ +#toctitle { + color: #ffffff; +} + +#toc ul { + display: inline; + list-style-type: none; + margin: 0; + padding: 0; + overflow: hidden; +} + +#toc li { + display: block; +} + +#toc a { + background-color: #333333; + float: left; + color: white; + text-align: center; + padding: 14px 16px; + text-decoration: none; +} + +#toc li a:hover { + background-color: #272727; + text-decoration: none; +} + +#toc:after { + content: " "; + visibility: hidden; + display: block; + height: 0; + clear: both; +} + +@media all and (max-width: 600px) { +table { + width: 55vw!important; + font-size: 3vw; +}} diff --git a/docs/stylesheets/clean.css b/docs/stylesheets/clean.css new file mode 100644 index 000000000000..ab9b986fb715 --- /dev/null +++ b/docs/stylesheets/clean.css @@ -0,0 +1,39 @@ +/* Asciidoctor default stylesheet | MIT License | http://asciidoctor.org */ + +@import "https://fonts.googleapis.com/css?family=Noto+Sans:300,600italic,400,400italic,600,600italic%7CNoto+Serif:400,400italic,700,700italic%7CDroid+Sans+Mono:400,700"; +@import "asciidoctor.css"; /* Default asciidoc style framework - important */ + +/* CUSTOMISATIONS */ +/* Change the values in root for quick customisation. If you want even more fine grain... venture further. */ + +:root{ +--maincolor:#FFFFFF; +--primarycolor:#2c3e50; +--secondarycolor:#ba3925; +--tertiarycolor: #186d7a; +--sidebarbackground:#CCC; +--linkcolor:#b71c1c; +--linkcoloralternate:#f44336; +--white:#FFFFFF; +--black:#000000; +} + +/* Text styles */ +h1{color:var(--primarycolor) !important;} +h2,h3,h4,h5,h6{color:var(--secondarycolor) !important;} +.title{color:var(--tertiarycolor) !important; font-family:"Noto Sans",sans-serif !important;font-style: normal !important; font-weight: normal !important;} +p{font-family: "Noto Sans",sans-serif !important} + +/* Table styles */ +th{font-family: "Noto Sans",sans-serif !important} + +/* Responsiveness fixes */ +video { + max-width: 100%; +} + +@media all and (max-width: 600px) { +table { + width: 55vw!important; + font-size: 3vw; +}} diff --git a/docs/stylesheets/dark.css b/docs/stylesheets/dark.css new file mode 100644 index 000000000000..0afb3f7a14e3 --- /dev/null +++ b/docs/stylesheets/dark.css @@ -0,0 +1,50 @@ +/* Asciidoctor default stylesheet | MIT License | http://asciidoctor.org */ + +@import url(https://fonts.googleapis.com/css?family=Noto+Sans); +@import "asciidoctor.css"; /* Default asciidoc style framework - important */ + +/* CUSTOMISATIONS */ +/* Change the values in root for quick customisation. If you want even more fine grain... venture further. */ +:root{ +--maincolor:#282c34; +--primarycolor:#f39c12; +--secondarycolor:#03a9f4; +--tertiarycolor:#4db6ac; +--sidebarbackground:#21252b; +--linkcolor:#f44336; +--linkcoloralternate:#ff9800; +--white:#FFFFFF; +} + +/* Text styles */ + +body{font-family: "Noto Sans",sans-serif;background-color: var(--maincolor);color:var(--white);} + +h1{color:var(--primarycolor) !important;font-family:"Noto Sans",sans-serif;} +h2,h3,h4,h5,h6{color:var(--secondarycolor) !important;font-family:"Noto Sans",sans-serif;} +.title{color:var(--white) !important;font-family:"Noto Sans",sans-serif;font-style: normal; font-weight: normal;} +p{font-family: "Noto Sans",sans-serif ! important} +#toc.toc2 a:link{color:var(--linkcolor);} +blockquote{color:var(--tertiarycolor) !important} +.quoteblock{color:var(--white)} +code{color:var(--linkcoloralternate);background-color: var(--sidebarbackground) !important} + + +/* Table styles */ +th{background-color: var(--maincolor);color:var(--white) !important;} +td{background-color: var(--maincolor);color: var(--linkcoloralternate) !important} + + +#toc.toc2{background-color:var(--sidebarbackground);} +#toctitle{color:var(--white);} + +/* Responsiveness fixes */ +video { + max-width: 100%; +} + +@media all and (max-width: 600px) { +table { + width: 55vw!important; + font-size: 3vw; +}} diff --git a/docs/stylesheets/fedora.css b/docs/stylesheets/fedora.css new file mode 100644 index 000000000000..51d37088f800 --- /dev/null +++ b/docs/stylesheets/fedora.css @@ -0,0 +1,53 @@ +/* Asciidoctor default stylesheet | MIT License | http://asciidoctor.org */ + +@import url(https://fonts.googleapis.com/css?family=Montserrat|Open+Sans); +@import "asciidoctor.css"; /* Default asciidoc style framework - important */ + +/* CUSTOMISATIONS */ +/* Change the values in root for quick customisation. If you want even more fine grain... venture further. */ +:root{ +--maincolor:#FFFFFF; +--primarycolor:#294172; /* Fedora Dark Blue */ +--secondarycolor:#3c6eb4; /* Fedora Blue */ +--tertiarycolor:#CCCCCC; +--highlightcolor:#e59728; /* Features Orange */ +--sidebarbackground:#CACACA; +--linkcolor:#0D47A1; +--linkcoloralternate:#db3279; /* Friends Magneta */ +--white:#FFFFFF; +--black:#000000; +} + +/* Text styles */ + +body{font-family: "Open Sans",sans-serif;background-color: var(--maincolor);color:var(--black);} + +h1{color:var(--primarycolor) !important;font-family:"Montserrat",sans-serif;} +h2,h3,h4,h5,h6{color:var(--secondarycolor) !important;font-family:"Montserrat",sans-serif;} +.title{color:var(--black) !important;font-family:"Open Sans",sans-serif;font-style: normal; font-weight: normal;} +a{text-decoration: none;} +p{font-family: "Open Sans",sans-serif ! important} +#toc.toc2 a:link{color:var(--linkcolor);} +blockquote{color:var(--linkcoloralternate) !important} +.quoteblock blockquote:before{color:var(--linkcoloralternate)} +code{color:var(--white);background-color: var(--highlightcolor) !important} +mark{background-color: var(--highlightcolor)} /* Text highlighting color */ + +/* Table styles */ +th{background-color: var(--maincolor);color:var(--black) !important;} +td{background-color: var(--maincolor);color: var(--black) !important} + + +#toc.toc2{background-color:var(--sidebarbackground);} +#toctitle{color:var(--white);} + +/* Responsiveness fixes */ +video { + max-width: 100%; +} + +@media all and (max-width: 600px) { +table { + width: 55vw!important; + font-size: 3vw; +}} diff --git a/docs/stylesheets/gazette.css b/docs/stylesheets/gazette.css new file mode 100644 index 000000000000..14e9bab5a00b --- /dev/null +++ b/docs/stylesheets/gazette.css @@ -0,0 +1,148 @@ +/* Asciidoctor default stylesheet | MIT License | http://asciidoctor.org */ + +@import url(https://fonts.googleapis.com/css?family=Source+Serif+Pro|UnifrakturMaguntia|Source+Sans+Pro); +@import "asciidoctor.css"; /* Default asciidoc style framework - important */ + +/* CUSTOMISATIONS */ +/* Change the values in root for quick customisation. If you want even more fine grain... venture further. */ +:root{ +--maincolor:#FFFFFF; +--primarycolor:#000000; +--secondarycolor:#AAAAAA; +--tertiarycolor:#CCCCCC; +--sidebarbackground:#FFFFFF; +--linkcolor:#0D47A1; +--linkcoloralternate:#B71C1C; +} + +/* Text styles */ + +body { + font-family: "Source Serif Pro", serif; + background-color: var(--maincolor); + color: black; +} + +h1,h2,h3,h4,h5,h6 { + color: var(--primarycolor) !important; +} +h1 { + font-family: "UnifrakturMaguntia", serif; +} +#header h1 { + border-bottom: 2px solid black!important; +} +h2 { + font-family: "Source Serif Pro", serif; + font-style: italic; + font-weight: bold; + text-transform: uppercase; +} +h3 { + font-family: "Source Serif Pro", serif; + font-style: italic; + font-weight: bold; +} +h4 { + font-family:'Times New Roman',Times,serif; + letter-spacing: -1px; + text-transform: uppercase; +} +h5 { + font-family:'Times New Roman',Times,serif; + letter-spacing: -1px; + text-transform: uppercase; +} +h6 { + font-family: "Source Serif Pro", serif; + font-weight: bold; +} +hr { + border-color: black; +} + +.title { + color: black !important; + font-family: "Source Serif Pro", serif; + font-style: normal; + font-weight: normal; +} +a { + text-decoration: none; + color: #5A5A5A!important; +} +p { + font-family: "Source Serif Pro", serif !important; +} +#toc.toc2 a:link { + color: var(--linkcolor); + font-family: "Source Serif Pro", serif; +} +blockquote { + color: var(--primarycolor) !important; +} +.quoteblock { + color: black; +} +.quoteblock blockquote:before { + color: black; +} +code { + color: white; + background-color: var(--secondarycolor) !important; +} +mark { + background-color: var(--tertiarycolor); +} /* Text highlighting color */ + +cite { + color: var(--primarycolor)!important; +} + +pre { + background-color: var(--maincolor)!important; +} +code { +/* background-color: var(--maincolor)!important; */ +} + +img { + -webkit-filter: grayscale(100%); + filter: grayscale(100%); +} +video { + -webkit-filter: grayscale(100%); + filter: grayscale(100%); +} + +/* Table styles */ +th { + background-color: var(--maincolor); + color: black !important; +} +td { + background-color: var(--maincolor); + color: black !important; +} + + +#toc.toc2 { + background-color: var(--sidebarbackground); +} +#toctitle { + color: black; + font-family: "Source Serif Pro", serif; + font-weight: bold; + padding-top: 20px; +} + +/* Responsiveness fixes */ +video { + max-width: 100%; +} + +@media all and (max-width: 600px) { +table { + width: 55vw!important; + font-size: 3vw; +}} diff --git a/docs/stylesheets/gh-dominic-pages.css b/docs/stylesheets/gh-dominic-pages.css new file mode 100644 index 000000000000..eeb71937861f --- /dev/null +++ b/docs/stylesheets/gh-dominic-pages.css @@ -0,0 +1,214 @@ +@import url(https://fonts.googleapis.com/css?family=Montserrat|Open+Sans); +@import "boot-lumen-dominic.css"; /* Default asciidoc style framework - important */ + +/* Custom block: details */ + +.sidebarblock.details > .content { + border-left: .25rem solid rgba(0, 153, 153, 153); +} + +.sidebarblock.details > .content { + padding-left: .5rem +} + +.sidebarblock.details { + background-color: transparent; + border: none; + padding-bottom: 0; + padding-top: 0; +} + +/* Overrides for asciidoctor.css */ + +a { + color: #006666; +} + +h1, +#content h1 > a.link, +h2, +h2 > a.link, +h3, +h3 > a.link, +#toctitle, +#toctitle > a.link, +.sidebarblock > .content > .title, +.sidebarblock > .content > .title > a.link, +h4, +h4 > a.link, +h5, +h5 > a.link, +h6, +h6 > a.link { + color: #006666; +} + +.subheader, +.admonitionblock td.content > .title, +.audioblock > .title, +.exampleblock > .title, +.imageblock > .title, +.listingblock > .title, +.literalblock > .title, +.stemblock > .title, +.openblock > .title, +.paragraph >.title, +.quoteblock > .title, +table.tableblock > .title, +.verseblock > .title, +.videoblock > .title, +.dlist > .title, +.olist > .title, +.ulist > .title, +.qlist > .title, +.hdlist > .title { + color: rgb(0, 102, 102); +} + +@media screen { + #footer { + background-color: #f6f6f6; + border-top: 1px #d2d2d2 solid; + border-bottom: 1px #d2d2d2 solid; + font-family: "Open Sans", "DejaVu Sans", sans-serif; + } + + #footer-text { + color: #ff0066; + line-height: 1; + } +} + +/* Utilities */ + +.container { + width: 100%; + max-width: 62.5rem; + margin-left: auto; + margin-right: auto; +} + +/* Colors */ + +.bg-light { + background-color: #f8f9fa; +} + +.bg-lighter { + background-color: #fbfbfb; +} + + +/* Navbar */ + +.navbar { + display: flex; + flex-wrap: nowrap; + justify-content: center; + font-family: "Open Sans", "DejaVu Sans", sans-serif; + font-size: 1rem; + padding: 0px 1rem; +} + +.navbar-lg { + font-size: 1.3rem; +} + +.navbar-light { + border-bottom: 1px #d2d2d2 solid; +} + +.navbar a { + text-decoration: none; +} + +.navbar-light a { + color: #ff0066; +} + +.navbar-light a:hover, +.navbar-light a:focus { + color: #009999; +} + +.navbar a.active, +.navbar a.active:hover, +.navbar a.active:focus { + font-weight: bold; +} + +.navbar-light a.active, +.navbar-light a.active:hover, +.navbar-light a.active:focus { + color: #009999; +} + +.navbar-light .nav-link { + border-bottom: 2px transparent solid; +} + +.navbar-light .nav-link.active { + border-bottom: 2px #009999 solid; +} + +.navbar-lg .nav-link.active { + border-bottom: 0; +} + +.navbar > .container { + display: flex; + flex-wrap: wrap; + justify-content: center; +} + +.navbar-brand { + display: inline-block; + margin-right: 1rem; + padding: 0.8125rem 0rem; + padding-left: 0.9375rem; + font-size: 1.25rem; +} + +.navbar-brand img { + height: 1.4rem; + margin: 0rem 0.4rem; + padding: 0; + vertical-align: middle; +} + +.navbar-lg .navbar-brand { + font-size: 1.7rem; +} + +.navbar-lg .navbar-brand img { + height: 2.3rem; +} + +.navbar-nav { + display: flex; + flex-wrap: wrap; + flex-grow: 1; + align-items: center; + margin: 0px; + padding: 0px; + list-style: none; + line-height: inherit; +} + +.nav-link { + display: block; + margin: 0px; + border: 0px; + padding: 1rem 1rem; +} + +/* Do not display site header on print mediums */ +@media print { + #seedu-header { + display: none; + } + + #site-header { + display: none; + } +} diff --git a/docs/stylesheets/gh-jacobhan-pages.css b/docs/stylesheets/gh-jacobhan-pages.css new file mode 100644 index 000000000000..eeb71937861f --- /dev/null +++ b/docs/stylesheets/gh-jacobhan-pages.css @@ -0,0 +1,214 @@ +@import url(https://fonts.googleapis.com/css?family=Montserrat|Open+Sans); +@import "boot-lumen-dominic.css"; /* Default asciidoc style framework - important */ + +/* Custom block: details */ + +.sidebarblock.details > .content { + border-left: .25rem solid rgba(0, 153, 153, 153); +} + +.sidebarblock.details > .content { + padding-left: .5rem +} + +.sidebarblock.details { + background-color: transparent; + border: none; + padding-bottom: 0; + padding-top: 0; +} + +/* Overrides for asciidoctor.css */ + +a { + color: #006666; +} + +h1, +#content h1 > a.link, +h2, +h2 > a.link, +h3, +h3 > a.link, +#toctitle, +#toctitle > a.link, +.sidebarblock > .content > .title, +.sidebarblock > .content > .title > a.link, +h4, +h4 > a.link, +h5, +h5 > a.link, +h6, +h6 > a.link { + color: #006666; +} + +.subheader, +.admonitionblock td.content > .title, +.audioblock > .title, +.exampleblock > .title, +.imageblock > .title, +.listingblock > .title, +.literalblock > .title, +.stemblock > .title, +.openblock > .title, +.paragraph >.title, +.quoteblock > .title, +table.tableblock > .title, +.verseblock > .title, +.videoblock > .title, +.dlist > .title, +.olist > .title, +.ulist > .title, +.qlist > .title, +.hdlist > .title { + color: rgb(0, 102, 102); +} + +@media screen { + #footer { + background-color: #f6f6f6; + border-top: 1px #d2d2d2 solid; + border-bottom: 1px #d2d2d2 solid; + font-family: "Open Sans", "DejaVu Sans", sans-serif; + } + + #footer-text { + color: #ff0066; + line-height: 1; + } +} + +/* Utilities */ + +.container { + width: 100%; + max-width: 62.5rem; + margin-left: auto; + margin-right: auto; +} + +/* Colors */ + +.bg-light { + background-color: #f8f9fa; +} + +.bg-lighter { + background-color: #fbfbfb; +} + + +/* Navbar */ + +.navbar { + display: flex; + flex-wrap: nowrap; + justify-content: center; + font-family: "Open Sans", "DejaVu Sans", sans-serif; + font-size: 1rem; + padding: 0px 1rem; +} + +.navbar-lg { + font-size: 1.3rem; +} + +.navbar-light { + border-bottom: 1px #d2d2d2 solid; +} + +.navbar a { + text-decoration: none; +} + +.navbar-light a { + color: #ff0066; +} + +.navbar-light a:hover, +.navbar-light a:focus { + color: #009999; +} + +.navbar a.active, +.navbar a.active:hover, +.navbar a.active:focus { + font-weight: bold; +} + +.navbar-light a.active, +.navbar-light a.active:hover, +.navbar-light a.active:focus { + color: #009999; +} + +.navbar-light .nav-link { + border-bottom: 2px transparent solid; +} + +.navbar-light .nav-link.active { + border-bottom: 2px #009999 solid; +} + +.navbar-lg .nav-link.active { + border-bottom: 0; +} + +.navbar > .container { + display: flex; + flex-wrap: wrap; + justify-content: center; +} + +.navbar-brand { + display: inline-block; + margin-right: 1rem; + padding: 0.8125rem 0rem; + padding-left: 0.9375rem; + font-size: 1.25rem; +} + +.navbar-brand img { + height: 1.4rem; + margin: 0rem 0.4rem; + padding: 0; + vertical-align: middle; +} + +.navbar-lg .navbar-brand { + font-size: 1.7rem; +} + +.navbar-lg .navbar-brand img { + height: 2.3rem; +} + +.navbar-nav { + display: flex; + flex-wrap: wrap; + flex-grow: 1; + align-items: center; + margin: 0px; + padding: 0px; + list-style: none; + line-height: inherit; +} + +.nav-link { + display: block; + margin: 0px; + border: 0px; + padding: 1rem 1rem; +} + +/* Do not display site header on print mediums */ +@media print { + #seedu-header { + display: none; + } + + #site-header { + display: none; + } +} diff --git a/docs/stylesheets/gh-pages.css b/docs/stylesheets/gh-pages.css index 121cac3885fd..6f13efc0b0e0 100644 --- a/docs/stylesheets/gh-pages.css +++ b/docs/stylesheets/gh-pages.css @@ -1,3 +1,4 @@ +//@@author ngkaicong @import url(https://fonts.googleapis.com/css?family=Montserrat|Open+Sans); @import "asciidoctor.css"; /* Default asciidoc style framework - important */ diff --git a/docs/stylesheets/gh-yushao-pages.css b/docs/stylesheets/gh-yushao-pages.css new file mode 100644 index 000000000000..f3bf533b0efa --- /dev/null +++ b/docs/stylesheets/gh-yushao-pages.css @@ -0,0 +1,214 @@ +@import url(https://fonts.googleapis.com/css?family=Montserrat|Open+Sans); +@import "boot-lumen-yushao.css"; /* Default asciidoc style framework - important */ + +/* Custom block: details */ + +.sidebarblock.details > .content { + border-left: .25rem solid rgba(0, 153, 153, 153); +} + +.sidebarblock.details > .content { + padding-left: .5rem +} + +.sidebarblock.details { + background-color: transparent; + border: none; + padding-bottom: 0; + padding-top: 0; +} + +/* Overrides for asciidoctor.css */ + +a { + color: #006666; +} + +h1, +#content h1 > a.link, +h2, +h2 > a.link, +h3, +h3 > a.link, +#toctitle, +#toctitle > a.link, +.sidebarblock > .content > .title, +.sidebarblock > .content > .title > a.link, +h4, +h4 > a.link, +h5, +h5 > a.link, +h6, +h6 > a.link { + color: #006666; +} + +.subheader, +.admonitionblock td.content > .title, +.audioblock > .title, +.exampleblock > .title, +.imageblock > .title, +.listingblock > .title, +.literalblock > .title, +.stemblock > .title, +.openblock > .title, +.paragraph >.title, +.quoteblock > .title, +table.tableblock > .title, +.verseblock > .title, +.videoblock > .title, +.dlist > .title, +.olist > .title, +.ulist > .title, +.qlist > .title, +.hdlist > .title { + color: rgb(0, 102, 102); +} + +@media screen { + #footer { + background-color: #f6f6f6; + border-top: 1px #d2d2d2 solid; + border-bottom: 1px #d2d2d2 solid; + font-family: "Open Sans", "DejaVu Sans", sans-serif; + } + + #footer-text { + color: #ff0066; + line-height: 1; + } +} + +/* Utilities */ + +.container { + width: 100%; + max-width: 62.5rem; + margin-left: auto; + margin-right: auto; +} + +/* Colors */ + +.bg-light { + background-color: #f8f9fa; +} + +.bg-lighter { + background-color: #fbfbfb; +} + + +/* Navbar */ + +.navbar { + display: flex; + flex-wrap: nowrap; + justify-content: center; + font-family: "Open Sans", "DejaVu Sans", sans-serif; + font-size: 1rem; + padding: 0px 1rem; +} + +.navbar-lg { + font-size: 1.3rem; +} + +.navbar-light { + border-bottom: 1px #d2d2d2 solid; +} + +.navbar a { + text-decoration: none; +} + +.navbar-light a { + color: #ff0066; +} + +.navbar-light a:hover, +.navbar-light a:focus { + color: #009999; +} + +.navbar a.active, +.navbar a.active:hover, +.navbar a.active:focus { + font-weight: bold; +} + +.navbar-light a.active, +.navbar-light a.active:hover, +.navbar-light a.active:focus { + color: #009999; +} + +.navbar-light .nav-link { + border-bottom: 2px transparent solid; +} + +.navbar-light .nav-link.active { + border-bottom: 2px #009999 solid; +} + +.navbar-lg .nav-link.active { + border-bottom: 0; +} + +.navbar > .container { + display: flex; + flex-wrap: wrap; + justify-content: center; +} + +.navbar-brand { + display: inline-block; + margin-right: 1rem; + padding: 0.8125rem 0rem; + padding-left: 0.9375rem; + font-size: 1.25rem; +} + +.navbar-brand img { + height: 1.4rem; + margin: 0rem 0.4rem; + padding: 0; + vertical-align: middle; +} + +.navbar-lg .navbar-brand { + font-size: 1.7rem; +} + +.navbar-lg .navbar-brand img { + height: 2.3rem; +} + +.navbar-nav { + display: flex; + flex-wrap: wrap; + flex-grow: 1; + align-items: center; + margin: 0px; + padding: 0px; + list-style: none; + line-height: inherit; +} + +.nav-link { + display: block; + margin: 0px; + border: 0px; + padding: 1rem 1rem; +} + +/* Do not display site header on print mediums */ +@media print { + #seedu-header { + display: none; + } + + #site-header { + display: none; + } +} diff --git a/docs/stylesheets/italian-pop.css b/docs/stylesheets/italian-pop.css new file mode 100644 index 000000000000..958d3cae8377 --- /dev/null +++ b/docs/stylesheets/italian-pop.css @@ -0,0 +1,45 @@ +/* Asciidoctor default stylesheet | MIT License | http://asciidoctor.org */ + +@import url(https://fonts.googleapis.com/css?family=Galada|Lato); +@import "asciidoctor.css"; /* Default asciidoc style framework - important */ + +/* CUSTOMISATIONS */ +/* Change the values in root for quick customisation. If you want even more fine grain... venture further. */ +:root{ +--maincolor:#FFFFFF; +--primarycolor:#b71c1c; +--sidebarbackground:#CCC; +--linkcolor:#b71c1c; +--linkcoloralternate:#f44336; +--white:#FFFFFF; +--black:#000000; +--grey:#212121; +} + +body{font-family: "Lato",sans-serif;background-color: var(--maincolor);color:var(--black);} + +h1,h2,h3,h4,h5,h6{color:var(--primarycolor) !important;font-family:"Galada",sans-serif;} +.title{color:var(--black) !important;font-family:"Galada",sans-serif;font-style: normal; font-weight: normal;} +p{font-family: "Lato",sans-serif ! important;color:var(--grey)} +#toc.toc2 a:link{color:var(--linkcolor);} + + +/* Table styles */ +th{background-color: var(--primarycolor);color:var(--black) !important;color:var(--white) !important} +td{color: var(--black)} +tr:nth-child(even) {background-color: #FFF !important} +tr:nth-child(odd) {background: #CCC !important} + +#toc.toc2{background-color:var(--sidebarbackground);} +#toctitle{color:var(--white);} + +/* Responsiveness fixes */ +video { + max-width: 100%; +} + +@media all and (max-width: 600px) { +table { + width: 55vw!important; + font-size: 3vw; +}} diff --git a/docs/stylesheets/material-amber.css b/docs/stylesheets/material-amber.css new file mode 100644 index 000000000000..5415af62faa8 --- /dev/null +++ b/docs/stylesheets/material-amber.css @@ -0,0 +1,67 @@ +/* Asciidoctor default stylesheet | MIT License | http://asciidoctor.org */ + +@import url(https://fonts.googleapis.com/css?family=Noto+Sans); +@import "asciidoctor.css"; /* Default asciidoc style framework - important */ + +/* CUSTOMISATIONS */ +/* Change the values in root for quick customisation. If you want even more fine grain... venture further. */ + +:root{ +--maincolor:#FFFFFF; +--primarycolor:#FFC107; /* Amber 500 */ +--secondarycolor:#ba3925; +--tertiarycolor: #186d7a; +--sidebarbackground:#FF6F00; /* Amber 900 */ +--linkcolor:#FFE082; /* Amber 200 */ +--linkcoloralternate:#f44336; +--white:#FFFFFF; +--black:#000000; +} + +body{font-family: "Noto Sans",sans-serif;} + +#header{background-color:var(--primarycolor); padding:25px;max-width: none;} +#footer{background-color: var(--sidebarbackground);} +h1,h2,h3{background-color:var(--primarycolor);color:var(--white) !important;font-family:"Noto Sans",sans-serif;text-decoration:none;padding:10px;} +h4,h5,h6{color:var(--primarycolor);} +.title{color:var(--sidebarbackground) !important;font-family:"Noto Sans",sans-serif;font-style: normal; font-weight: normal;} +p{font-family: "Noto Sans",sans-serif ! important} +#toc.toc2 a:link{color:white;} + +a { + text-decoration: none; + color: var(--linkcolor); +} +a:hover { + color: var(--sidebarbackground); +} +.quoteblock blockquote::before { + color: var(--linkcolor); +} +mark { + color: var(--white); + background-color: var(--linkcolor); +} + +/* Card styling */ +.sect1{border-bottom:1px solid grey;border-radius:8px;} + +/* Table styles */ +th{background-color: var(--linkcolor);color:#FFFFFF;} + +#toc.toc2{background-color:var(--sidebarbackground);color:white !important;} +#toc.toc2.a{color:var(--white);} +#toc.toc2.a:active{color:var(--white) !important;} +#toc.toc2.a:visited{color:var(--white) !important;} +#toctitle{color:white;font-size: 16px;} + +/* Responsiveness fixes */ +video { + max-width: 100%; +} + +@media all and (max-width: 600px) { +table { + width: 55vw!important; + font-size: 3vw; +}} diff --git a/docs/stylesheets/material-blue.css b/docs/stylesheets/material-blue.css new file mode 100644 index 000000000000..2275a3e55db5 --- /dev/null +++ b/docs/stylesheets/material-blue.css @@ -0,0 +1,67 @@ +/* Asciidoctor default stylesheet | MIT License | http://asciidoctor.org */ + +@import url(https://fonts.googleapis.com/css?family=Noto+Sans); +@import "asciidoctor.css"; /* Default asciidoc style framework - important */ + +/* CUSTOMISATIONS */ +/* Change the values in root for quick customisation. If you want even more fine grain... venture further. */ + +:root{ +--maincolor:#FFFFFF; +--primarycolor:#2196F3; /* Blue 500 */ +--secondarycolor:#ba3925; +--tertiarycolor: #186d7a; +--sidebarbackground:#0D47A1; /* Blue 900 */ +--linkcolor:#90CAF9; /* Blue 200 */ +--linkcoloralternate:#f44336; +--white:#FFFFFF; +--black:#000000; +} + +body{font-family: "Noto Sans",sans-serif;} + +#header{background-color:var(--primarycolor); padding:25px;max-width: none;} +#footer{background-color: var(--sidebarbackground);} +h1,h2,h3{background-color:var(--primarycolor);color:var(--white) !important;font-family:"Noto Sans",sans-serif;text-decoration:none;padding:10px;} +h4,h5,h6{color:var(--primarycolor);} +.title{color:var(--sidebarbackground) !important;font-family:"Noto Sans",sans-serif;font-style: normal; font-weight: normal;} +p{font-family: "Noto Sans",sans-serif ! important} +#toc.toc2 a:link{color:white;} + +a { + text-decoration: none; + color: var(--linkcolor); +} +a:hover { + color: var(--sidebarbackground); +} +.quoteblock blockquote::before { + color: var(--linkcolor); +} +mark { + color: var(--white); + background-color: var(--linkcolor); +} + +/* Card styling */ +.sect1{border-bottom:1px solid grey;border-radius:8px;} + +/* Table styles */ +th{background-color: var(--linkcolor);color:#FFFFFF;} + +#toc.toc2{background-color:var(--sidebarbackground);color:white !important;} +#toc.toc2.a{color:var(--white);} +#toc.toc2.a:active{color:var(--white) !important;} +#toc.toc2.a:visited{color:var(--white) !important;} +#toctitle{color:white;font-size: 16px;} + +/* Responsiveness fixes */ +video { + max-width: 100%; +} + +@media all and (max-width: 600px) { +table { + width: 55vw!important; + font-size: 3vw; +}} diff --git a/docs/stylesheets/material-brown.css b/docs/stylesheets/material-brown.css new file mode 100644 index 000000000000..113eb05ec998 --- /dev/null +++ b/docs/stylesheets/material-brown.css @@ -0,0 +1,67 @@ +/* Asciidoctor default stylesheet | MIT License | http://asciidoctor.org */ + +@import url(https://fonts.googleapis.com/css?family=Noto+Sans); +@import "asciidoctor.css"; /* Default asciidoc style framework - important */ + +/* CUSTOMISATIONS */ +/* Change the values in root for quick customisation. If you want even more fine grain... venture further. */ + +:root{ +--maincolor:#FFFFFF; +--primarycolor:#795548; /* Brown 500 */ +--secondarycolor:#ba3925; +--tertiarycolor: #186d7a; +--sidebarbackground:#3E2723; /* Brown 900 */ +--linkcolor:#BCAAA4; /* Brown 200 */ +--linkcoloralternate:#f44336; +--white:#FFFFFF; +--black:#000000; +} + +body{font-family: "Noto Sans",sans-serif;} + +#header{background-color:var(--primarycolor); padding:25px;max-width: none;} +#footer{background-color: var(--sidebarbackground);} +h1,h2,h3{background-color:var(--primarycolor);color:var(--white) !important;font-family:"Noto Sans",sans-serif;text-decoration:none;padding:10px;} +h4,h5,h6{color:var(--primarycolor);} +.title{color:var(--sidebarbackground) !important;font-family:"Noto Sans",sans-serif;font-style: normal; font-weight: normal;} +p{font-family: "Noto Sans",sans-serif ! important} +#toc.toc2 a:link{color:white;} + +a { + text-decoration: none; + color: var(--linkcolor); +} +a:hover { + color: var(--sidebarbackground); +} +.quoteblock blockquote::before { + color: var(--linkcolor); +} +mark { + color: var(--white); + background-color: var(--linkcolor); +} + +/* Card styling */ +.sect1{border-bottom:1px solid grey;border-radius:8px;} + +/* Table styles */ +th{background-color: var(--linkcolor);color:#FFFFFF;} + +#toc.toc2{background-color:var(--sidebarbackground);color:white !important;} +#toc.toc2.a{color:var(--white);} +#toc.toc2.a:active{color:var(--white) !important;} +#toc.toc2.a:visited{color:var(--white) !important;} +#toctitle{color:white;font-size: 16px;} + +/* Responsiveness fixes */ +video { + max-width: 100%; +} + +@media all and (max-width: 600px) { +table { + width: 55vw!important; + font-size: 3vw; +}} diff --git a/docs/stylesheets/material-green.css b/docs/stylesheets/material-green.css new file mode 100644 index 000000000000..0922e95c7cc8 --- /dev/null +++ b/docs/stylesheets/material-green.css @@ -0,0 +1,67 @@ +/* Asciidoctor default stylesheet | MIT License | http://asciidoctor.org */ + +@import url(https://fonts.googleapis.com/css?family=Noto+Sans); +@import "asciidoctor.css"; /* Default asciidoc style framework - important */ + +/* CUSTOMISATIONS */ +/* Change the values in root for quick customisation. If you want even more fine grain... venture further. */ + +:root{ +--maincolor:#FFFFFF; +--primarycolor:#4CAF50; /* Green 500 */ +--secondarycolor:#ba3925; +--tertiarycolor: #186d7a; +--sidebarbackground:#1B5E20; /* Green 900 */ +--linkcolor:#A5D6A7; /* Green 200 */ +--linkcoloralternate:#f44336; +--white:#FFFFFF; +--black:#000000; +} + +body{font-family: "Noto Sans",sans-serif;} + +#header{background-color:var(--primarycolor); padding:25px;max-width: none;} +#footer{background-color: var(--sidebarbackground);} +h1,h2,h3{background-color:var(--primarycolor);color:var(--white) !important;font-family:"Noto Sans",sans-serif;text-decoration:none;padding:10px;} +h4,h5,h6{color:var(--primarycolor);} +.title{color:var(--sidebarbackground) !important;font-family:"Noto Sans",sans-serif;font-style: normal; font-weight: normal;} +p{font-family: "Noto Sans",sans-serif ! important} +#toc.toc2 a:link{color:white;} + +a { + text-decoration: none; + color: var(--linkcolor); +} +a:hover { + color: var(--sidebarbackground); +} +.quoteblock blockquote::before { + color: var(--linkcolor); +} +mark { + color: var(--white); + background-color: var(--linkcolor); +} + +/* Card styling */ +.sect1{border-bottom:1px solid grey;border-radius:8px;} + +/* Table styles */ +th{background-color: var(--linkcolor);color:#FFFFFF;} + +#toc.toc2{background-color:var(--sidebarbackground);color:white !important;} +#toc.toc2.a{color:var(--white);} +#toc.toc2.a:active{color:var(--white) !important;} +#toc.toc2.a:visited{color:var(--white) !important;} +#toctitle{color:white;font-size: 16px;} + +/* Responsiveness fixes */ +video { + max-width: 100%; +} + +@media all and (max-width: 600px) { +table { + width: 55vw!important; + font-size: 3vw; +}} diff --git a/docs/stylesheets/material-grey.css b/docs/stylesheets/material-grey.css new file mode 100644 index 000000000000..266f4084db81 --- /dev/null +++ b/docs/stylesheets/material-grey.css @@ -0,0 +1,67 @@ +/* Asciidoctor default stylesheet | MIT License | http://asciidoctor.org */ + +@import url(https://fonts.googleapis.com/css?family=Noto+Sans); +@import "asciidoctor.css"; /* Default asciidoc style framework - important */ + +/* CUSTOMISATIONS */ +/* Change the values in root for quick customisation. If you want even more fine grain... venture further. */ + +:root{ +--maincolor:#FFFFFF; +--primarycolor:#9E9E9E; /* Grey 500 */ +--secondarycolor:#ba3925; +--tertiarycolor: #186d7a; +--sidebarbackground:#212121; /* Grey 900 */ +--linkcolor:#EEEEEE; /* Grey 200 */ +--linkcoloralternate:#f44336; +--white:#FFFFFF; +--black:#000000; +} + +body{font-family: "Noto Sans",sans-serif;} + +#header{background-color:var(--primarycolor); padding:25px;max-width: none;} +#footer{background-color: var(--sidebarbackground);} +h1,h2,h3{background-color:var(--primarycolor);color:var(--white) !important;font-family:"Noto Sans",sans-serif;text-decoration:none;padding:10px;} +h4,h5,h6{color:var(--primarycolor);} +.title{color:var(--sidebarbackground) !important;font-family:"Noto Sans",sans-serif;font-style: normal; font-weight: normal;} +p{font-family: "Noto Sans",sans-serif ! important} +#toc.toc2 a:link{color:white;} + +a { + text-decoration: none; + color: var(--linkcolor); +} +a:hover { + color: var(--sidebarbackground); +} +.quoteblock blockquote::before { + color: var(--linkcolor); +} +mark { + color: var(--white); + background-color: var(--linkcolor); +} + +/* Card styling */ +.sect1{border-bottom:1px solid grey;border-radius:8px;} + +/* Table styles */ +th{background-color: var(--linkcolor);color:#FFFFFF;} + +#toc.toc2{background-color:var(--sidebarbackground);color:white !important;} +#toc.toc2.a{color:var(--white);} +#toc.toc2.a:active{color:var(--white) !important;} +#toc.toc2.a:visited{color:var(--white) !important;} +#toctitle{color:white;font-size: 16px;} + +/* Responsiveness fixes */ +video { + max-width: 100%; +} + +@media all and (max-width: 600px) { +table { + width: 55vw!important; + font-size: 3vw; +}} diff --git a/docs/stylesheets/material-orange.css b/docs/stylesheets/material-orange.css new file mode 100644 index 000000000000..dd8a8a06c7f2 --- /dev/null +++ b/docs/stylesheets/material-orange.css @@ -0,0 +1,67 @@ +/* Asciidoctor default stylesheet | MIT License | http://asciidoctor.org */ + +@import url(https://fonts.googleapis.com/css?family=Noto+Sans); +@import "asciidoctor.css"; /* Default asciidoc style framework - important */ + +/* CUSTOMISATIONS */ +/* Change the values in root for quick customisation. If you want even more fine grain... venture further. */ + +:root{ +--maincolor:#FFFFFF; +--primarycolor:#FF9800; /* Orange 500 */ +--secondarycolor:#ba3925; +--tertiarycolor: #186d7a; +--sidebarbackground:#E65100; /* Orange 900 */ +--linkcolor:#FFCC80; /* Orange 200 */ +--linkcoloralternate:#f44336; +--white:#FFFFFF; +--black:#000000; +} + +body{font-family: "Noto Sans",sans-serif;} + +#header{background-color:var(--primarycolor); padding:25px;max-width: none;} +#footer{background-color: var(--sidebarbackground);} +h1,h2,h3{background-color:var(--primarycolor);color:var(--white) !important;font-family:"Noto Sans",sans-serif;text-decoration:none;padding:10px;} +h4,h5,h6{color:var(--primarycolor);} +.title{color:var(--sidebarbackground) !important;font-family:"Noto Sans",sans-serif;font-style: normal; font-weight: normal;} +p{font-family: "Noto Sans",sans-serif ! important} +#toc.toc2 a:link{color:white;} + +a { + text-decoration: none; + color: var(--linkcolor); +} +a:hover { + color: var(--sidebarbackground); +} +.quoteblock blockquote::before { + color: var(--linkcolor); +} +mark { + color: var(--white); + background-color: var(--linkcolor); +} + +/* Card styling */ +.sect1{border-bottom:1px solid grey;border-radius:8px;} + +/* Table styles */ +th{background-color: var(--linkcolor);color:#FFFFFF;} + +#toc.toc2{background-color:var(--sidebarbackground);color:white !important;} +#toc.toc2.a{color:var(--white);} +#toc.toc2.a:active{color:var(--white) !important;} +#toc.toc2.a:visited{color:var(--white) !important;} +#toctitle{color:white;font-size: 16px;} + +/* Responsiveness fixes */ +video { + max-width: 100%; +} + +@media all and (max-width: 600px) { +table { + width: 55vw!important; + font-size: 3vw; +}} diff --git a/docs/stylesheets/material-pink.css b/docs/stylesheets/material-pink.css new file mode 100644 index 000000000000..f0c88c036144 --- /dev/null +++ b/docs/stylesheets/material-pink.css @@ -0,0 +1,67 @@ +/* Asciidoctor default stylesheet | MIT License | http://asciidoctor.org */ + +@import url(https://fonts.googleapis.com/css?family=Noto+Sans); +@import "asciidoctor.css"; /* Default asciidoc style framework - important */ + +/* CUSTOMISATIONS */ +/* Change the values in root for quick customisation. If you want even more fine grain... venture further. */ + +:root{ +--maincolor:#FFFFFF; +--primarycolor:#E91E63; /* Pink 500 */ +--secondarycolor:#ba3925; +--tertiarycolor: #186d7a; +--sidebarbackground:#880E4F; /* Pink 900 */ +--linkcolor:#F48FB1; /* Pink 200 */ +--linkcoloralternate:#f44336; +--white:#FFFFFF; +--black:#000000; +} + +body{font-family: "Noto Sans",sans-serif;} + +#header{background-color:var(--primarycolor); padding:25px;max-width: none;} +#footer{background-color: var(--sidebarbackground);} +h1,h2,h3{background-color:var(--primarycolor);color:var(--white) !important;font-family:"Noto Sans",sans-serif;text-decoration:none;padding:10px;} +h4,h5,h6{color:var(--primarycolor);} +.title{color:var(--sidebarbackground) !important;font-family:"Noto Sans",sans-serif;font-style: normal; font-weight: normal;} +p{font-family: "Noto Sans",sans-serif ! important} +#toc.toc2 a:link{color:white;} + +a { + text-decoration: none; + color: var(--linkcolor); +} +a:hover { + color: var(--sidebarbackground); +} +.quoteblock blockquote::before { + color: var(--linkcolor); +} +mark { + color: var(--white); + background-color: var(--linkcolor); +} + +/* Card styling */ +.sect1{border-bottom:1px solid grey;border-radius:8px;} + +/* Table styles */ +th{background-color: var(--linkcolor);color:#FFFFFF;} + +#toc.toc2{background-color:var(--sidebarbackground);color:white !important;} +#toc.toc2.a{color:var(--white);} +#toc.toc2.a:active{color:var(--white) !important;} +#toc.toc2.a:visited{color:var(--white) !important;} +#toctitle{color:white;font-size: 16px;} + +/* Responsiveness fixes */ +video { + max-width: 100%; +} + +@media all and (max-width: 600px) { +table { + width: 55vw!important; + font-size: 3vw; +}} diff --git a/docs/stylesheets/material-purple.css b/docs/stylesheets/material-purple.css new file mode 100644 index 000000000000..f16106b81d1c --- /dev/null +++ b/docs/stylesheets/material-purple.css @@ -0,0 +1,67 @@ +/* Asciidoctor default stylesheet | MIT License | http://asciidoctor.org */ + +@import url(https://fonts.googleapis.com/css?family=Noto+Sans); +@import "asciidoctor.css"; /* Default asciidoc style framework - important */ + +/* CUSTOMISATIONS */ +/* Change the values in root for quick customisation. If you want even more fine grain... venture further. */ + +:root{ +--maincolor:#FFFFFF; +--primarycolor:#9C27B0; /* Purple 500 */ +--secondarycolor:#ba3925; +--tertiarycolor: #186d7a; +--sidebarbackground:#4A148C; /* Purple 900 */ +--linkcolor:#CE93D8; /* Purple 200 */ +--linkcoloralternate:#f44336; +--white:#FFFFFF; +--black:#000000; +} + +body{font-family: "Noto Sans",sans-serif;} + +#header{background-color:var(--primarycolor); padding:25px;max-width: none;} +#footer{background-color: var(--sidebarbackground);} +h1,h2,h3{background-color:var(--primarycolor);color:var(--white) !important;font-family:"Noto Sans",sans-serif;text-decoration:none;padding:10px;} +h4,h5,h6{color:var(--primarycolor);} +.title{color:var(--sidebarbackground) !important;font-family:"Noto Sans",sans-serif;font-style: normal; font-weight: normal;} +p{font-family: "Noto Sans",sans-serif ! important} +#toc.toc2 a:link{color:white;} + +a { + text-decoration: none; + color: var(--linkcolor); +} +a:hover { + color: var(--sidebarbackground); +} +.quoteblock blockquote::before { + color: var(--linkcolor); +} +mark { + color: var(--white); + background-color: var(--linkcolor); +} + +/* Card styling */ +.sect1{border-bottom:1px solid grey;border-radius:8px;} + +/* Table styles */ +th{background-color: var(--linkcolor);color:#FFFFFF;} + +#toc.toc2{background-color:var(--sidebarbackground);color:white !important;} +#toc.toc2.a{color:var(--white);} +#toc.toc2.a:active{color:var(--white) !important;} +#toc.toc2.a:visited{color:var(--white) !important;} +#toctitle{color:white;font-size: 16px;} + +/* Responsiveness fixes */ +video { + max-width: 100%; +} + +@media all and (max-width: 600px) { +table { + width: 55vw!important; + font-size: 3vw; +}} diff --git a/docs/stylesheets/material-red.css b/docs/stylesheets/material-red.css new file mode 100644 index 000000000000..ad426e06fd1a --- /dev/null +++ b/docs/stylesheets/material-red.css @@ -0,0 +1,67 @@ +/* Asciidoctor default stylesheet | MIT License | http://asciidoctor.org */ + +@import url(https://fonts.googleapis.com/css?family=Noto+Sans); +@import "asciidoctor.css"; /* Default asciidoc style framework - important */ + +/* CUSTOMISATIONS */ +/* Change the values in root for quick customisation. If you want even more fine grain... venture further. */ + +:root{ +--maincolor:#FFFFFF; +--primarycolor:#F44336; /* Red 500 */ +--secondarycolor:#ba3925; +--tertiarycolor: #186d7a; +--sidebarbackground:#B71C1C; /* Red 900 */ +--linkcolor:#EF9A9A; /* Red 200 */ +--linkcoloralternate:#f44336; +--white:#FFFFFF; +--black:#000000; +} + +body{font-family: "Noto Sans",sans-serif;} + +#header{background-color:var(--primarycolor); padding:25px;max-width: none;} +#footer{background-color: var(--sidebarbackground);} +h1,h2,h3{background-color:var(--primarycolor);color:var(--white) !important;font-family:"Noto Sans",sans-serif;text-decoration:none;padding:10px;} +h4,h5,h6{color:var(--primarycolor);} +.title{color:var(--sidebarbackground) !important;font-family:"Noto Sans",sans-serif;font-style: normal; font-weight: normal;} +p{font-family: "Noto Sans",sans-serif ! important} +#toc.toc2 a:link{color:white;} + +a { + text-decoration: none; + color: var(--linkcolor); +} +a:hover { + color: var(--sidebarbackground); +} +.quoteblock blockquote::before { + color: var(--linkcolor); +} +mark { + color: var(--white); + background-color: var(--linkcolor); +} + +/* Card styling */ +.sect1{border-bottom:1px solid grey;border-radius:8px;} + +/* Table styles */ +th{background-color: var(--linkcolor);color:#FFFFFF;} + +#toc.toc2{background-color:var(--sidebarbackground);color:white !important;} +#toc.toc2.a{color:var(--white);} +#toc.toc2.a:active{color:var(--white) !important;} +#toc.toc2.a:visited{color:var(--white) !important;} +#toctitle{color:white;font-size: 16px;} + +/* Responsiveness fixes */ +video { + max-width: 100%; +} + +@media all and (max-width: 600px) { +table { + width: 55vw!important; + font-size: 3vw; +}} diff --git a/docs/stylesheets/material-teal.css b/docs/stylesheets/material-teal.css new file mode 100644 index 000000000000..45d4913c95bc --- /dev/null +++ b/docs/stylesheets/material-teal.css @@ -0,0 +1,67 @@ +/* Asciidoctor default stylesheet | MIT License | http://asciidoctor.org */ + +@import url(https://fonts.googleapis.com/css?family=Noto+Sans); +@import "asciidoctor.css"; /* Default asciidoc style framework - important */ + +/* CUSTOMISATIONS */ +/* Change the values in root for quick customisation. If you want even more fine grain... venture further. */ + +:root{ +--maincolor:#FFFFFF; +--primarycolor:#009688; /* Teal 500 */ +--secondarycolor:#ba3925; +--tertiarycolor: #186d7a; +--sidebarbackground:#004d40; /* Teal 900 */ +--linkcolor:#80cbc4; /* Teal 200 */ +--linkcoloralternate:#f44336; +--white:#FFFFFF; +--black:#000000; +} + +body{font-family: "Noto Sans",sans-serif;} + +#header{background-color:var(--primarycolor); padding:25px;max-width: none;} +#footer{background-color: var(--sidebarbackground);} +h1,h2,h3{background-color:var(--primarycolor);color:var(--white) !important;font-family:"Noto Sans",sans-serif;text-decoration:none;padding:10px;} +h4,h5,h6{color:var(--primarycolor);} +.title{color:var(--sidebarbackground) !important;font-family:"Noto Sans",sans-serif;font-style: normal; font-weight: normal;} +p{font-family: "Noto Sans",sans-serif ! important} +#toc.toc2 a:link{color:white;} + +a { + text-decoration: none; + color: var(--linkcolor); +} +a:hover { + color: var(--sidebarbackground); +} +.quoteblock blockquote::before { + color: var(--linkcolor); +} +mark { + color: var(--white); + background-color: #80cbc4; +} + +/* Card styling */ +.sect1{border-bottom:1px solid grey;border-radius:8px;} + +/* Table styles */ +th{background-color: #80cbc4;color:#FFFFFF;} + +#toc.toc2{background-color:var(--sidebarbackground);color:white !important;} +#toc.toc2.a{color:var(--white);} +#toc.toc2.a:active{color:var(--white) !important;} +#toc.toc2.a:visited{color:var(--white) !important;} +#toctitle{color:white;font-size: 16px;} + +/* Responsiveness fixes */ +video { + max-width: 100%; +} + +@media all and (max-width: 600px) { +table { + width: 55vw!important; + font-size: 3vw; +}} diff --git a/docs/stylesheets/medium.css b/docs/stylesheets/medium.css new file mode 100644 index 000000000000..e910a8de829c --- /dev/null +++ b/docs/stylesheets/medium.css @@ -0,0 +1,176 @@ +body { + font-family: Georgia,Cambria,"Times New Roman",Times,serif; + font-weight: 400; + font-size: 21px!important; + line-height: 1.58; + letter-spacing: -.003em; + margin-top: 29px; + width: 90vh; + margin: 0 auto; + padding-left: 5vw!important; +} + +h1 { + margin-top: 0; + font-family: "Lucida Grande","Lucida Sans Unicode","Lucida Sans",Geneva,Arial,sans-serif; + font-weight: 700; + font-style: normal; + font-size: 36px; + margin-left: -2.25px; + line-height: 1.15; + letter-spacing: -.02em; + color: rgba(0,0,0,.8); + word-wrap: break-word; +} +h1 { + border-bottom: 0!important; +} + +h2, h3, h4, h5, h6 { + margin-top: 39px; + font-family: "Lucida Grande","Lucida Sans Unicode","Lucida Sans",Geneva,Arial,sans-serif; + font-weight: 300; + font-style: normal; + margin-left: -1.75px; + line-height: 1.22; + letter-spacing: -.022em; + color: rgba(0,0,0,.44); +} +h2 { + font-size: 28px; +} +h3 { + font-size: 26px; +} +h4 { + font-size: 24px; +} +h5 { + font-size: 22px; +} +h6 { + font-size: 20px; +} + +#toc { + display: none; +} + +blockquote { + margin-top: 55px; + font-family: Georgia,Cambria,"Times New Roman",Times,serif; + font-weight: 400; + font-style: italic; + font-size: 28px; + margin-left: -1.75px; + line-height: 1.48; + letter-spacing: -.014em; + color: rgba(0,0,0,.6); + border: none; + padding: 0; + padding-left: 50px; + text-align: left; +} +blockquote:before { + color: rgba(0,0,0,.6)!important; +} + +.byline { + font-style: italic; + font-weight: 700; + font-family: Georgia,Cambria,"Times New Roman",Times,serif; + font-size: 21px; + line-height: 1.58; + letter-spacing: -.003em; + color: rgba(0,0,0,.8); +} + +.blurb { + font-family: Georgia,Cambria,"Times New Roman",Times,serif; + font-weight: 400; + font-size: 21px; + line-height: 1.58; + letter-spacing: -.003em; +} + +hr { + margin-top: 52px; + margin-bottom: 42px; + border: 0; + text-align: center; +} +hr:before { + font-family: Georgia,Cambria,"Times New Roman",Times,serif; + font-weight: 400; + font-style: italic; + font-size: 28px; + letter-spacing: .6em; + content: '...'; + display: inline-block; + margin-left: .6em; + color: rgba(0,0,0,.6); + position: relative; + top: -30px; +} +a { + text-decoration: none; + background-image: linear-gradient(to bottom,rgba(0,0,0,0) 50%,rgba(0,0,0,.6) 50%); + background-repeat: repeat-x; + background-size: 2px 2px; + background-position: 0 22px; + color: inherit; + background-color: transparent; +} +a:hover { + outline: 0; + color: inherit; +} +p { + font-size: inherit; +} + +.title { + font-style: normal!important; + font-family: "Lucida Grande","Lucida Sans Unicode","Lucida Sans",Geneva,Arial,sans-serif!important; + font-weight: 300!important; + font-size: 13px!important; + line-height: 1.4!important; + color: rgba(0,0,0,.6)!important; + letter-spacing: 0!important; + text-align: center!important; + margin-top: 10px!important; +} +.title a { + text-decoration: none; + background-image: linear-gradient(to bottom,rgba(0,0,0,.44) 50%,#fff 50%); + background-size: 2px 2px; + background-position: 0 14px; + background-repeat: repeat-x; + font-size: 13px!important; +} +.title a:hover { + outline: 0; + color: inherit; +} + +tfoot .tableblock { + font-weight: bold; +} + +/* Responsiveness fixes */ +img { + max-width: 100%; +} +video { + max-width: 85vw; +} + +@media all and (max-width: 600px) { +table { + width: 55vw!important; + font-size: 3vw; +} + +pre { + font-size: large; +}} diff --git a/docs/stylesheets/monospace.css b/docs/stylesheets/monospace.css new file mode 100644 index 000000000000..287258579a43 --- /dev/null +++ b/docs/stylesheets/monospace.css @@ -0,0 +1,42 @@ +/* Asciidoctor default stylesheet | MIT License | http://asciidoctor.org */ + +@import 'https://fonts.googleapis.com/css?family=Source+Code+Pro'; +@import "asciidoctor.css"; /* Default asciidoc style framework - important */ + +/* CUSTOMISATIONS */ +/* Change the values in root for quick customisation. If you want even more fine grain... venture further. */ + +:root{ +--maincolor:#FFFFFF; +--primarycolor:#000000; +--secondarycolor:#000000; +--tertiarycolor: #000000; +--sidebarbackground:#CCC; +--linkcolor:#000000; +--linkcoloralternate:#f44336; +--white:#FFFFFF; +--black:#000000; +} + +body{font-family: "Source Code Pro",sans-serif;} + +/* Text styles */ +h1{color:var(--primarycolor) !important; font-family: "Source Code Pro",sans-serif;} +h2,h3,h4,h5,h6{color:var(--secondarycolor) !important; font-family: "Source Code Pro",sans-serif;} +.title{color:var(--tertiarycolor) !important; font-family:"Source Code Pro",sans-serif !important;font-style: normal !important; font-weight: normal !important;} + +/* Sidebar */ +#toctitle{font-family: "Source Code Pro",sans-serif;} +.sectlevel1{font-family: "Source Code Pro",sans-serif!important;} +.sectlevel2{font-family: "Source Code Pro",sans-serif!important;} + +/* Responsiveness fixes */ +video { + max-width: 100%; +} + +@media all and (max-width: 600px) { +table { + width: 55vw!important; + font-size: 3vw; +}} diff --git a/docs/stylesheets/notebook.css b/docs/stylesheets/notebook.css new file mode 100644 index 000000000000..2a5c2e05f484 --- /dev/null +++ b/docs/stylesheets/notebook.css @@ -0,0 +1,56 @@ +/* Asciidoctor default stylesheet | MIT License | http://asciidoctor.org */ + +@import url(https://fonts.googleapis.com/css?family=Cabin+Sketch|Architects+Daughter); +@import "asciidoctor.css"; /* Default asciidoc style framework - important */ + +/* CUSTOMISATIONS */ +/* Change the values in root for quick customisation. If you want even more fine grain... venture further. */ +:root{ +--maincolor:#FFFFFF; +--primarycolor:#000000; +--secondarycolor:#1a237e; +--tertiarycolor:#CCCCCC; +--highlightcolor: #ffd600; +--sidebarbackground:#CACACA; +--linkcolor:#0D47A1; +--linkcoloralternate:#B71C1C; +--stickynote: #f9a825; +--white:#FFFFFF; +--black:#000000; +} + +/* Text styles */ + +body{font-family: "Architects Daughter",sans-serif;background-color: #fff;background-image:linear-gradient(90deg, transparent 79px, #abced4 79px, #abced4 81px, transparent 81px),linear-gradient(#eee .15em, transparent .15em);background-size: 100% 1.2em;} + +h1{color:var(--primarycolor) !important;font-family:"Cabin Sketch",sans-serif;} +h2,h3,h4,h5,h6{color:var(--secondarycolor) !important;font-family:"Cabin Sketch",sans-serif;} +.title{color:var(--black) !important;font-family:"Architects Daughter",sans-serif;font-style: normal; font-weight: normal;} +/*a{text-decoration: none;}*/ +p{font-family: "Architects Daughter",sans-serif ! important} +#toc.toc2 a:link{color:var(--linkcolor); font-family: "Architects Daughter" !important} +blockquote{color:var(--secondarycolor) !important} +.quoteblock{color:var(--black)} +.quoteblock blockquote:before{color:var(--black)} +code{color:var(--highlightcolor);background-color: var(--black) !important} +mark{background-color: var(--highlightcolor)} /* Text highlighting color */ +pre{background-color: var(--stickynote) !important;color:var(--secondarycolor);font-family: monospace;} + +/* Table styles */ +th{background-color: var(--maincolor);color:var(--black) !important;} +td{background-color: var(--maincolor);color: var(--black) !important} + + +#toc.toc2{background-color:var(--sidebarbackground);font-family: "Architects Daughter",sans-serif;} +#toctitle{color:var(--white); font-family: "Cabin Sketch"} + +/* Responsiveness fixes */ +video { + max-width: 100%; +} + +@media all and (max-width: 600px) { +table { + width: 55vw!important; + font-size: 3vw; +}} diff --git a/docs/stylesheets/plain.css b/docs/stylesheets/plain.css new file mode 100644 index 000000000000..0708315074d5 --- /dev/null +++ b/docs/stylesheets/plain.css @@ -0,0 +1,53 @@ +/* Asciidoctor default stylesheet | MIT License | http://asciidoctor.org */ + +@import url(https://fonts.googleapis.com/css?family=Noto+Sans); +@import "asciidoctor.css"; /* Default asciidoc style framework - important */ + +/* CUSTOMISATIONS */ +/* Change the values in root for quick customisation. If you want even more fine grain... venture further. */ +:root{ +--maincolor:#FFFFFF; +--primarycolor:#000000; +--secondarycolor:#AAAAAA; +--tertiarycolor:#CCCCCC; +--sidebarbackground:#CACACA; +--linkcolor:#0D47A1; +--linkcoloralternate:#B71C1C; +--white:#FFFFFF; +--black:#000000; +} + +/* Text styles */ + +body{font-family: "Noto Sans",sans-serif;background-color: var(--maincolor);color:var(--black);} + +h1{color:var(--primarycolor) !important;font-family:"Noto Sans",sans-serif;} +h2,h3,h4,h5,h6{color:var(--secondarycolor) !important;font-family:"Noto Sans",sans-serif;} +.title{color:var(--black) !important;font-family:"Noto Sans",sans-serif;font-style: normal; font-weight: normal;} +a{text-decoration: none;} +p{font-family: "Noto Sans",sans-serif ! important} +#toc.toc2 a:link{color:var(--linkcolor);} +blockquote{color:var(--secondarycolor) !important} +.quoteblock{color:var(--black)} +.quoteblock blockquote:before{color:var(--black)} +code{color:var(--white);background-color: var(--secondarycolor) !important} +mark{background-color: var(--tertiarycolor)} /* Text highlighting color */ + +/* Table styles */ +th{background-color: var(--maincolor);color:var(--black) !important;} +td{background-color: var(--maincolor);color: var(--black) !important} + + +#toc.toc2{background-color:var(--sidebarbackground);} +#toctitle{color:var(--white);} + +/* Responsiveness fixes */ +video { + max-width: 100%; +} + +@media all and (max-width: 600px) { +table { + width: 55vw!important; + font-size: 3vw; +}} diff --git a/docs/stylesheets/template.css b/docs/stylesheets/template.css new file mode 100644 index 000000000000..fe0b3adc131d --- /dev/null +++ b/docs/stylesheets/template.css @@ -0,0 +1,216 @@ +/* document body (contains all content) */ +body { + font-size: small; +} + +/* document header (contains title etc) */ +#header { + width: 100%; +} + +/* headings */ +h1 { + color: purple; +} +h2 { + color: chartreuse; +} +h3 { + color: coral; +} +h4 { + color: darkcyan; +} +h5 { + color: darkslategray; +} +h6 { + color: olive; +} + +/* Table of Contents sidebar */ +#toc { + background-color: plum!important; + color: white; + font-weight: bold; +} +/* title of the TOC */ +#toctitle { + color: white; +} +/* top-level entries in TOC */ +.sectlevel1 { + background-color: palegoldenrod; +} +/* second-level entries in TOC */ +.sectlevel2 { + background-color: palegreen; +} + +/* main content window */ +#content { + background-color: lavender; + color: navy; +} + +/* plain paragraph text */ +.paragraph { + font-family: sans-serif; +} +p { + font-family: sans-serif; +} + +/* blockquote text */ +.quoteblock { + font-style: italic; +} +blockquote { + font-style: italic; +} + +/* the quotation mark itself (before the block) */ +.quoteblock blockquote::before { + color: blue; +} + +/* blockquote attribution text */ +.attribution { + font-size: x-large; +} + +/* blockquote citation (work where quote cited) */ +cite { + font-size: x-large; +} + +/* ordered list */ +ol { + color: red; +} +.olist { + color: red; +} + +/* unordered list */ +ul { + color: blue; +} +.ulist { + color: blue; +} + +/* links */ +a { + text-decoration: none; +} + +/* bold text */ +strong { + color: green; +} + +/* italic text */ +em { + color: orange; +} + +/* underlined text */ +u { + color: yellow; +} + +/* deleted text */ +del { + text-decoration: line-through; + color: red; +} +/* inserted text */ +ins { + text-decoration: overline; + color: green; +} + +/* strikethrough text */ +s { + text-decoration-color: red; +} + +/* superscript text */ +sup {} +/* subscript text */ +sub {} + +/* small text */ +small {} + +/* highlighted text */ +mark {} + +/* horizontal rules */ +hr {} + +/* table */ +table {} +/* table caption */ +caption {} +/* table header row */ +thead {} +/* table header cell */ +th {} +/* table row */ +tr {} +/* table footer */ +tfoot {} +/* table cell */ +td {} +/* table body */ +tbody {} + +/* inline code */ +code { + background-color: papayawhip!important; +} +/* pre-formatted text */ +pre { + background-color: burlywood!important; +} +.literalblock { + background-color: burlywood!important; +} + +/* image */ +img { + max-width: 100%; +} +/* image caption */ +.imageblock .title { + font-weight: bold!important; +} + +/* audio */ +audio {} +/* video */ +video {} + +/* footer section */ +#footer { + background-color: gray; + color: red; +} +/* footer text (by default contains time of last document update) */ +#footer-text { + font-weight: bold; + color: white; +} + +/* Responsiveness fixes */ +video { + max-width: 100%; +} + +@media all and (max-width: 600px) { +table { + width: 55vw!important; + font-size: 3vw; +}} diff --git a/docs/stylesheets/ubuntu.css b/docs/stylesheets/ubuntu.css new file mode 100644 index 000000000000..31317bd16440 --- /dev/null +++ b/docs/stylesheets/ubuntu.css @@ -0,0 +1,48 @@ +/* Asciidoctor default stylesheet | MIT License | http://asciidoctor.org */ + +@import url(https://fonts.googleapis.com/css?family=Ubuntu); +@import "asciidoctor.css"; /* Default asciidoc style framework - important */ + +/* CUSTOMISATIONS */ +/* Change the values in root for quick customisation. If you want even more fine grain... venture further. */ + +:root{ +--maincolor:#FFFFFF; +--primarycolor:#E95420; +--secondarycolor:#333333; +--tertiarycolor: #772953; +--sidebarbackground:#CCC; +--linkcolor:#b71c1c; +--linkcoloralternate:#f44336; +--white:#FFFFFF; +--black:#000000; +} + +/* Text styles */ +body{font-family: "Ubuntu",sans-serif;} + +h1,h2{color:var(--primarycolor) !important;font-family:"Ubuntu",sans-serif;} +h3,h4,h5,h6{color:var(--secondarycolor);font-family: "Ubuntu",sans-serif;} +.title{color:var(--primarycolor) !important;font-family:"Ubuntu",sans-serif;font-style: normal; font-weight: normal;} +p{font-family: "Ubuntu",sans-serif ! important} +#toc.toc2 a:link{color:white;} +code{background-color: var(--secondarycolor) !important;color:var(--white)} + + +/* Table styles */ +th{background-color: var(--tertiarycolor);color:var(--white) !important;} + +#toc.toc2{background-color:#2C001E;color:white;} +#toc.toc2.a{color:white;} +#toctitle{color:#E95420;} + +/* Responsiveness fixes */ +video { + max-width: 100%; +} + +@media all and (max-width: 600px) { +table { + width: 55vw!important; + font-size: 3vw; +}} diff --git a/docs/team/dominic.adoc b/docs/team/dominic.adoc new file mode 100644 index 000000000000..50710de8496c --- /dev/null +++ b/docs/team/dominic.adoc @@ -0,0 +1,121 @@ += Dominic - Project Portfolio +:site-section: AboutUs +:imagesDir: ../images +:stylesDir: ../stylesheets/ +:stylesheet: gh-dominic-pages.css +:sectnums: + +== Project: 'Budgeteer' - A Desktop Financial Manager to track expenses and incomes so as to better understand savings and earnings for financial freedom + +== Overview + +=== Project & Product Information + +'Budgeteer' is a desktop financial planner application my team of 4 created for our Software Engineering Principles module. +Our main project was to morph an existing code into any other software we opted to. We were evaluated based on our ability to work with existing base code and the quality of our new modifications to +the existing code. + +Budgeteer has a command-line interface (CLI) and graphical user interface created using JavaFX. It is for users who enjoy typing to manage their finances responsibly using a desktop interface. +The application revolves around usage of the CLI to manage one's finances. Users can store an `entry` consisting of +`name`, `cashflow`, `date` and `tags` of any form of financial activity in the application. + +== Summary of Contributions + +*_Major enhancements_* : Morphed the AddressBook4 to Budgeteer. + +** `Reason for enhancement`: To enable Budgeteer to keep track of the financial income and expenses of the person. +** `Highlights`: Time taken to read all 10 kLoc to understand and modify the code. +** `My contributions`: Re-factored functions together with variables across classes to make the components compatible and consistent for the application. +Managed to restructure some parts to make it more cohesive. + +*_Major Enhancement_*: ability to filter relevant financial data + +** *Function*: To provide additional options for user to search for other data entries. +** *Highlights*: Improves the product significantly for users who may not be remember all the data. With this filter, +they can sniff our relevant financial information that matter to them and +save time by not looking all the data one by one. + +** *Justification*: To create an effective and efficient way for user to access data that are important and relevant to them conveniently, and to +make sense of what they are spending and when and how much so as to properly make better financial decisions. + +*_Major Enhancement_*: added display of relevant sorted data + +** *Function*: allows users to sort their data by a specific type and in a specific order +** *Highlights*: This feature improves the product by providing users with different means of manipulating +and reorganising their data. This was a simple but much needed functionality. + +** *Justification*: To create an effective and efficient way for user to access data that are important and relevant to them conveniently, and to +make sense of what they are spending and when and how much so as to properly make better financial decisions. + +*_Minor Enhancement_#1*: ability to give suggestions in commandBox + +** *Function*: display a popup-box of suggested words to autocomplete with when user inputs parts of a word. +** *Highlights*: Improves the product significantly for users who may not be familiar with the software commands and can +save time by keying in parts of a command word and simply complete the command using the list of suggested words. + +** *Justification*: To create an effective autocomplete by words instead of entire text, I needed to accurately read +the the entire user input and provide a variable range of suggestions. While it may seem a simple feature, I wrote an extensive amount of code for this feature to ensure the auto complete +performed its role successfully for most of the functions in our software. + + +*_Minor Enhancement_#2*: ability to prevent tampering of data by locking it + +** *Function*: it secures the application via passwords and data encryption +** *Highlights*: The implementation of a password not only provided a layer of security but a means to prove the identity of the user. +It locks and encrypts the password that is cannot be viewed. The implementation of a password requires an extensive analysis of design alternatives. +** *Justification*: Financial data is important as it reveals spending and earning pattern that can be sold to marketers. Needs to be protected. + + +=== *Other Contributions* + +** [https://nuscs2113-ay1819s2.github.io/dashboard-beta/#search=&sort=displayName&since=2019-02-10&until=2019-04-12&timeframe=day&reverse=false&repoSort=true] + +* *_Project management_* : +** Raised milestones and organized the group to complete each milestones before deadline. +** Ensured that milestone objectives were met and submitted to relevant PRs to module repo +** Support for issues tracker and PRs +** Managed releases for milestones + +* *_Documentation_* : +** Did cosmetic tweaks to existing contents of the User Guide and Developer Guide. +** Proof-read, organised documentation to maintain its coherence. + +* *_Testing_*: +** Wrote extensive tests to increase coverage of the project + +* *_Tools_* : +** Integrated a third party library to the project and the team repo + +* *_Debugging team members code_* : +** Provided ideas for possible implementation for other team member's features + +* *_Team Management_* : +** Coordinated some team meetings +** Ensured that team members meet feature deadlines set within the team + +== Contributions to the User Guide + +|=== +|_Given below are sections I contributed to the <<../UserGuide, User Guide>>. They showcase my ability to write documentation targeting end-users._ +|=== + +include::../UserGuide.adoc[tag=lock] + +include::../UserGuide.adoc[tag=filter] + +include::../UserGuide.adoc[tag=display] + +== Contributions to the Developer Guide + +|=== +|_Given below are sections I contributed to the Developer Guide. They showcase my ability to write technical documentation and the technical depth of my contributions to the project._ +|=== + +include::../DeveloperGuide.adoc[tag=CommandsUISupport] + +include::../DeveloperGuide.adoc[tag=lock] + +include::../DeveloperGuide.adoc[tag=filter] + +include::../DeveloperGuide.adoc[tag=display] + diff --git a/docs/team/jacobhan.adoc b/docs/team/jacobhan.adoc new file mode 100644 index 000000000000..6a70df3e9223 --- /dev/null +++ b/docs/team/jacobhan.adoc @@ -0,0 +1,100 @@ += Jacob Han - Project Portfolio +:site-section: AboutUs +:imagesDir: ../images +:stylesDir: ../stylesheets/ +:stylesheet: gh-jacobhan-pages.css + + +== Project: 'Budgeteer' - A Desktop Financial Manager to track expenses and incomes so as to better understand savings and earnings for financial freedom + +== Overview + +=== Project & Product Information + +'Budgeteer' is a desktop financial planner application my team of 4 created for our Software Engineering Principles module. +Our main project was to morph an existing code into any other software we opted to. We were evaluated based on our ability to work with existing base code and the quality of our new modifications to +the existing code. + +Budgeteer has a command-line interface (CLI) and graphical user interface created using JavaFX. It is for users who enjoy typing to manage their finances responsibly using a desktop interface. +The application revolves around usage of the CLI to manage one's finances. Users can store an `entry` consisting of `name`, `cashflow`, `date` and `tags` of any form of financial activity in the application. + +== Summary of Contributions + +*_Major enhancements_* : Added feature to return purchasing power and price of an entered stock + +** `Reason for enhancement`: People who use Budgeteer responsibly to record earnings and costs may want to know about how to turn potential savings into stock investments. Rather than using a separate application to get stock prices and to calculate how much you can purchase, it's possible to access this information from the command line so that you can immediately see how your savings can turn into investments. +** `Highlights`: The implementation required extensive use of the cash flow and report implementation, and required parsing information from an external API. I had to read the documentation of the Alpha Vantage API to figure out how to retrieve the data and implement it within the application. +** `My contributions`: Created new command ('stock'), parser, and methods that interacted with existing ones as well as the Alpha Vantage API to return stock purchasing power and prices. +** `Credits`: The Alpha Vantage API was incorporated to implement this feature. + +*_Major enhancements_* : Added feature to return purchasing power and price of an entered cryptocurrency + +** `Reason for enhancement`: People who use Budgeteer responsibly to record earnings and costs may want to know about how to turn potential savings into cryptocurrency investments. Rather than using a separate application to get cryptocurrency prices and to calculate how much you can purchase, it's possible to access this information from the command line so that you can immediately see how your savings can turn into investment. +** `Highlights`: The implementation required extensive use of the cash flow and report implementation, and required parsing information from an external API. I had to read the documentation of the Crypto Compare API to figure out how to retrieve the data and implement it within the application. +** `My contributions`: Created new command ('crypto'), parser, and methods that interacted with existing ones as well as the Crypto Compare API to return cryptocurrency purchasing power and prices +** `Credits`: The Crypto Compare API was incorporated to implement this feature. + +*_Minor enhancements_* : Added feature for rapid crypto returns of bitcoin, ethereum, and litecoin + +** `Reason for enhancement`: This is an extension of the earlier feature, but it can be time-consuming to wait for an API call for every command line input. Because a large part of the cryptocurrency market cap is held by a small number of cryptocurrencies, we can use threading for the top three (Bitcoin, Ethereum, Litecoin which hold nearly three-fourths of the total market cap) for rapid calls. This also makes it easier for people to enter in the commands without knowing the abbreviations for cryptocurrencies. +** `Highlights`: The implementation required extensive use of the cash flow and report implementation, and required parsing information from an external API. I had to read the documentation of the Crypto Compare API to figure out how to retrieve the data and implement it within the application. +** `My contributions`: Created new commands ('bitcoin', 'ethereum', 'litecoin'), parsers, and methods that interacted with existing ones as well as the Crypto Compare API to return cryptocurrency purchasing power and prices +** `Credits`: In addition to using the Crypto Compare API, this enhancement required threading. Yushao grealty helped me in the implementation of the threads. + +*_Minor enhancements_* : Added feature for calculating hypothetical long term balance based on interest rate and time + +** `Reason for enhancement`: For those who are more used to traditional investments, those who use Budgeter to record potential savings might want to know how much their savings would be worth in the long term if they are used for investments. They can see how much their savings would be worth based on a fixed interest rate over a certain amount of time. +** `Highlights`: The implementation required extensive use of the cash flow and report information, and implementation of the compound interest formula. +** `My contributions`: Created new commands ('invest'), parsers, and methods that interacted with existing ones to return hypothetical balance based on interest rate and time + +=== *Other Contributions* + +* *_Project management_* : +** Raised milestones and organized the group to complete each milestones before deadline. +** Ensured that milestone objectives were met and submitted to relevant PRs to module repo +** Support for issues tracker and PRs + +* *_Documentation_* : +** Did cosmetic tweaks to existing contents of the User Guide and Developer Guide. +** Proof-read, organised documentation to maintain its coherence. + +* *_Testing_*: +** Wrote tests to increase coverage of the project + +* *_Tools_* : +** Incorporated the Alpha Vantage and Crypto Compare APIs into commands, and Reposense for contributions + +* *_Team Management_* : +** Coordinated some team meetings + +== Contributions to the User Guide + +|=== +|_Given below are sections I contributed to the <<../UserGuide, User Guide>>. They showcase my ability to write documentation targeting end-users._ +|=== + +include::../UserGuide.adoc[tag=bitcoin] + +include::../UserGuide.adoc[tag=ethereum] + +include::../UserGuide.adoc[tag=litecoin] + +include::../UserGuide.adoc[tag=stock] + +include::../UserGuide.adoc[tag=crypto] + +include::../UserGuide.adoc[tag=invest] + +== Contributions to the Developer Guide + +|=== +|_Given below are sections I contributed to the Developer Guide. They showcase my ability to write technical documentation and the technical depth of my contributions to the project._ +|=== + +include::../DeveloperGuide.adoc[tag=stock] + +include::../DeveloperGuide.adoc[tag=crypto] + +include::../DeveloperGuide.adoc[tag=rapidcrypto] + +include::../DeveloperGuide.adoc[tag=invest] diff --git a/docs/team/johndoe.adoc b/docs/team/johndoe.adoc deleted file mode 100644 index 453c2152ab9d..000000000000 --- a/docs/team/johndoe.adoc +++ /dev/null @@ -1,72 +0,0 @@ -= John Doe - Project Portfolio -:site-section: AboutUs -:imagesDir: ../images -:stylesDir: ../stylesheets - -== PROJECT: AddressBook - Level 4 - ---- - -== Overview - -AddressBook - Level 4 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. - -== Summary of contributions - -* *Major enhancement*: 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}_ - -* *Minor enhancement*: added a history command that allows the user to navigate to previous commands using up/down keys. - -* *Code contributed*: [https://github.com[Functional code]] [https://github.com[Test code]] _{give links to collated code files}_ - -* *Other contributions*: - -** Project management: -*** Managed releases `v1.3` - `v1.5rc` (3 releases) on GitHub -** Enhancements to existing features: -*** Updated the GUI color scheme (Pull requests https://github.com[#33], https://github.com[#34]) -*** Wrote additional tests for existing features to increase coverage from 88% to 92% (Pull requests https://github.com[#36], https://github.com[#38]) -** Documentation: -*** Did cosmetic tweaks to existing contents of the User Guide: https://github.com[#14] -** Community: -*** PRs reviewed (with non-trivial review comments): https://github.com[#12], https://github.com[#32], https://github.com[#19], https://github.com[#42] -*** Contributed to forum discussions (examples: https://github.com[1], https://github.com[2], https://github.com[3], https://github.com[4]) -*** Reported bugs and suggestions for other teams in the class (examples: https://github.com[1], https://github.com[2], https://github.com[3]) -*** Some parts of the history feature I added was adopted by several other class mates (https://github.com[1], https://github.com[2]) -** Tools: -*** Integrated a third party library (Natty) to the project (https://github.com[#42]) -*** Integrated a new Github plugin (CircleCI) to the team repo - -_{you can add/remove categories in the list above}_ - -== Contributions to the User Guide - - -|=== -|_Given below are sections I contributed to the User Guide. They showcase my ability to write documentation targeting end-users._ -|=== - -include::../UserGuide.adoc[tag=undoredo] - -include::../UserGuide.adoc[tag=dataencryption] - -== Contributions to the Developer Guide - -|=== -|_Given below are sections I contributed to the Developer Guide. They showcase my ability to write technical documentation and the technical depth of my contributions to the project._ -|=== - -include::../DeveloperGuide.adoc[tag=undoredo] - -include::../DeveloperGuide.adoc[tag=dataencryption] - - -== PROJECT: PowerPointLabs - ---- - -_{Optionally, you may include other projects in your portfolio.}_ diff --git a/docs/team/kaicong.adoc b/docs/team/kaicong.adoc new file mode 100644 index 000000000000..a8264acae044 --- /dev/null +++ b/docs/team/kaicong.adoc @@ -0,0 +1,179 @@ +//@@author ngkaicong += Ng Kai Cong - Project Portfolio +:site-section: AboutUs +:imagesDir: ../images +:stylesDir: ../stylesheets/ +:stylesheet: gh-kaicong-pages.css + + +== Project: 'Budgeter' - A Desktop Financial Manager to track expenses and incomes so as to better understand savings and earnings for financial freedom + +== Overview + +=== Project & Product Information + +'Budgeter' is a desktop financial planner application my team of 4 created for our Software Engineering Principles module. +Our main project was to morph an existing code into any other software we opted to. + +Budgeter has a command-line interface (CLI) and graphical user interface created using JavaFX. It is for users who enjoy typing to manage their finances responsibly using a desktop interface. +The application revolves around usage of the CLI to manage one's finances. + +== Summary of Contributions + +*_Major enhancements_* : Function to export data from budgeter to excel file + +** `Reason for enhancement`: Some people are more familiar with using excel and prefer to see their finances on a spreadsheet. They may also want to store it somewhere outside of their current computer and keep it safe. +** `Highlights`: This major enhancement is not an easy implementation as the library is not included in Java 10 library, I had to read the documentation and figure how to implement my feature in a short time. +** `My contributions`: This excel exporting command can take in six kinds of inputs, which is complex and thus requires a long time to debug and test. +** `Credits`: Apache POI library was used to implement this feature. + +*_Major enhancements_* : Added a feature to draw a line chart in the summary tab to track financials in Excel file. + +** `Reason for enhancement`: After the user exports the data to the excel file, they may open the file at their convenience without the Budgeter and still see a visual representation of their financial status. +** `Highlights`:This major enhancement is not an easy implementation as the library is not included in Java 10 library, I had to read the documentation and figure how to implement my feature in a short time. +** `My contributions`: The line chart is automatically created after the budgeter exports the data to the Excel file +** `Credits`: Apache POI library was used to implement this feature. + + +Reason for enhancement: A line chart into Excel file will be drawn next to the summary data after executing the exportexcel or archive command. This line chart will offer the user a visual display of income, outcome and net financial status based on date. + +Highlights: : This major enhancement requires significant effort and time, as initially, the library is not included in Java 10 library, I have to read the documentation as well as learn how to utilize the library in the short time. Therefore, it is reasonable to consider this feature as advanced feature. + + +=== *Other Contributions* + +* *_Project management_* : +** Raised milestones and organized the group to complete each milestones before deadline. +** Ensured that milestone objectives were met and submitted to relevant PRs to module repo +** Support for issues tracker and PRs + +* *_Documentation_* : +** Did cosmetic tweaks to existing contents of the User Guide and Developer Guide. +** Proof-read, organised documentation to maintain its coherence. + +* *_Testing_*: +** Wrote tests to increase coverage of the project + +* *_Tools_* : +** Integrated a third party library to the project and the team repo + +* *_Team Management_* : +** Coordinated some team meetings + +== Contributions to the User Guide + +|=== +|_Given below are sections I contributed to the <<../UserGuide, User Guide>>. They showcase my ability to write documentation targeting end-users._ +|=== + +=== Export the entry data from Budgeter to the Excel file: `export` + +Exports the entries into an Excel file. + + +There are 6 modes, default mode, single argument mode and dual argument mode (for Date) and single argument mode (Directory Path). + + +Format: + + +**** +* *Default mode* `export` will list down all entries in Budgeter and exports all of them to an Excel file and store the file in the default *Working Directory*, it will *detect automatically user's Working Directory*. + +* *Single argument Date mode* `export d/DATE` will list down all entries with the specified date and exports all shown entries to an Excel file and store the file in the default *Working Directory*, it will *detect automatically user's Working Directory*. + +* *Dual argument Date mode* `export d/START_DATE END_DATE` will list down all entries with the date that fall on either dates or between both dates and exports all shown entries to an Excel file and store the file in the default *Working Directory*, it will *detect automatically user's Working Directory*. + +* *Single argument Directory Path mode* `export dir/DIRECTORY_PATH` will list down all entries in Budgeter and exports all of them to an Excel file and store the file in the chosen Directory Path. + +* *Single argument Date mode + Single argument Directory path mode* `export d/DATE dir/DIRECTORY_PATH` will list down all entries with the specified date and exports all shown entries to an Excel file and store the file in the chosen Directory Path. + +* *Dual argument Date mode + Single argument Directory path mode* `export d/START_DATE END_DATE dir/DIRECTORY_PATH` will list down all entries with the date that fall on either dates or between both dates and exports all shown entries to an Excel file and store the file in the chosen Directory Path. ++ +**** + +If the command is in *Dual argument Date mode*, START_DATE (the first `Date`) should be earlier than or equal to the END_DATE (the second `Date`). + +Date should follow the same configurations as date parameters required when adding entries. It is in the form of *dd-mm-yyyy* where *dd* represents day, *mm* represents month and *yyyy* represents the year. *dd* and *mm* both require 1 to 2 digits while *yyyy* requires exactly 4 digits. + +The Excel file name will be named based on the command, relating to Date: + + +* *Default mode*: The Excel file will be named `ENTRIES_ALL.xlsx` +* *Single argument Date mode*: The Excel file will be named `ENTRIES_dd-mm-yyyy.xlsx` +* *Dual argument Date*: The Excel file will be named `ENTRIES_dd-mm-yyyy_dd-mm-yyyy.xlsx` + +If the Excel file with the same name and stored in same Directory exists, it will be overwritten. However, it *must* be closed before we enter the command. + +After you enter the `export` command, you should *wait for few seconds* for the Excel file to be written. + +Please note that `undo` and `redo` command can only affect Budgeter but the *not* the Excel file created, meaning that when you enter `undo` command after you enter the `export` command, the Budgeter will inform the user that *No more command to undo*, the entries remain the same and the Excel file created will *not* be deleted. + +Examples: + +* `export` +* `export d/31-3-1999` +* `export dir/C:\` +* `export d/31-3-1999 31-03-2019` +* `export d/31-3-1999 dir/C:\` +* `export d/31-3-1999 31-3-2019 dir/C:\` + +// end::exportexcel[] + +// tag::draw_line_chart[] + +=== Creates line chart automatically inside the Excel sheet : `Requires no command` + +Automatically takes the summary data from the *SUMMARY DATA* tab in the Excel sheet after the command `export` is called and creates an line chart. +The screenshot below, in the *SUMMARY DATA* tab, shows the line chart. + +image::linechart2.png[width="500"] + +* On the top left of the chart shows the legend with 3 lines, namely Income, Expense, and Nett. +** The blue line shows the Income based on Date. +** The orange line shows the Expense based on Date +** The grey line shows the Nett (total of income and expense) based on Date. + +// end::draw_line_chart[] + +== Contributions to the Developer Guide + +|=== +|_Given below are sections I contributed to the Developer Guide. They showcase my ability to write technical documentation and the technical depth of my contributions to the project._ +|=== + +=== Export entries from Budgeter to Excel file. +==== Current implementation +The export into excel file mechanism is facilitated by `ModelManager` with the help of `ExcelUtil`, the utility created to handle all methods relating to Excel. It represents an in-memory model of the Budgeter and is the component which manages the interactions between the commands, `ExcelUtil` and the `VersionedEntriesBook`. ExportExcelCommand calls `ModelManager#updateFilteredEntries` and passes in different predicates depending on the argument mode. +The List is retrieved by calling `ModelManager#getFilteredEntryList`. Meanwhile, it also called `ModelManager#getEntriesBook` to get the `ReadOnlyEntriesBook`. The SummaryByDateList is constructed after the ReadOnlyEntriesBook together with the predicate are passed into the construction of SummaryByDateList. The List is easily retrieved from SummaryByDateList by calling `SummaryByDateList#getSummaryList`. `ExcelUtil#setNameExcelFile` is called to make the Excel name based on the condition of startDate and endDate. After that, `ExcelUtil#setPathFile` is called to set the Path file, which is the location of the Excel file stored in future. +The Path file is constructed based on the name of the Excel file we retrieve above and the directory Path, it can be either optionally entered by the user or the default *User's Working Directory*. With the sufficient information, `List entries`, `List summaryList`, `file path`, `ExportExcelCommand#exportDataIntoExcelSheetWithGivenEntries` is called to start the processing of producing Excel file. + +There are 6 modes for this feature [refer to *Export the entry data from Budgeter to the Excel file* part in *User Guide*]. The mechanism that facilitates these modes can be found in the `ExportExcelCommandParser#parse`. Below is a overview of the mechanism: + +. Method `ExportExcelCommandParser#createExportExcelCommand` takes the input argument and further analyse it. +. The input given by the user is passed to `ArgumentTokeniser#tokenise` to split the input separated by prefixes. +. This returns a `ArgumentMultiMap` which contains a map with prefixes as keys and their associated input arguments as the value. +. The string associated with `d/` +.. It is then passed into `ExportExcelCommandParser#splitByWhitespace` for further processing and returns an array. This string will be split into sub-strings and each of them will be construct as a date type variable. The the size of the array exceed 2, error wil be thrown to inform invalid command format. *If the size of the string equals 1*, it is constructed as a date type variable after being passed to `ParseUtil#parseDate`, it must follow the format dd-mm-yyyy. Error will be thrown if the format is *not* correct or the date entered is *not* real. *If the size of the string equals 2*, each sub-string is constructed as a date type variable after being passed to `ParseUtil#parseDate`, and an additional check is conducted to check if the first date entered, known as Start date is smaller than or equal to the second date entered, known as End Date. +. The String associated with `dir/` +.. It is then passed into `ParseUtil#parseDirectoryString` to check if the Directory path given is existing. *If the Directory path is unreal*, an error is thrown to inform the user. +. Please take note that: +.. If the prefix `d/` is *not* entered in the input, meaning that all the entries will be included in the Excel sheet. +.. If the prefix `dir/` is *not* entered in the input, meaning that the Directory Path is default as the *User's Working Directory*. + +The `ExportExcelCommand` has four constructors which makes use of overloading to reduce code complexity. + +* One constructor has no arguments and assigns default predicate for the `FilteredList` in `ModelManager`, +`PREDICATE_SHOW_ALL_ENTRIES` which will show all items in the list and the Directory path is *User's Working Directory*. +* The second constructor takes in 2 `Date` arguments and assigns the predicate `DateIsWithinDateIntervalPredicate` which will only show items within the date interval and the Directory path is *User's Working Directory*. +* The third constructor takes in 1 `Directory Path` argument and assigns the predicate as `PREDICATE_SHOW_ALL_ENTRIES`, which will show all items in the list and the Directory path is the entered directory path. +* The fourth constructor takes in 1 `Directory Path` and 2 `Date` arguments and assigns the predicate as `DateIsWithinDateIntervalPredicate` which will only show items within the date interval and the Directory path is the entered Directory Path. + +If the Excel file with the same name and stored in same Directory exists, it will be overwritten. However, it *must* be closed before we enter the command. + + +// end::exportexcel[] + +// tag::draw_line_chart[] + +=== Draw a line chart automatically inside the Excel sheet +==== Current implementation + +This feature will automatically uses the the summary data from the `SUMMARY DATA` sheet in the Excel sheet after the command `export` is typed by user. +The feature mechanism is facilitated by `ExcelUtil`, which handles all methods related to Excel. It is the component which manages the interactions between the ExportExcelCommand with `ExcelUtil#drawChart`. + +// end::draw_line_chart[] diff --git a/docs/team/yushao.adoc b/docs/team/yushao.adoc new file mode 100644 index 000000000000..4c54f44ce9dc --- /dev/null +++ b/docs/team/yushao.adoc @@ -0,0 +1,110 @@ += Pang Yu Shao - Project Portfolio +:site-section: AboutUs +:imagesDir: ../images +:stylesDir: ../stylesheets/ +:stylesheet: gh-yushao-pages.css + + +== Project: 'Budgeteer' - A Desktop Financial Manager to track expenses and incomes so as to better understand savings and earnings for financial freedom + +== Overview + +=== Project & Product Information + +'Budgeteer' is a desktop financial planner application my team of 4 created for our Software Engineering Principles module. +Our main project was to morph an existing code into any other software we opted to. We were evaluated based on our ability to work with existing base code and the quality of our new modifications to +the existing code. + +Budgeteer has a command-line interface (CLI) and graphical user interface created using JavaFX. It is for users who enjoy typing to manage their finances responsibly using a desktop interface. +The application revolves around usage of the CLI to manage one's finances. Users can store an `entry` consisting of +`name`, `cashflow`, `date` and `tags` of any form of financial activity in the application. + +== Summary of Contributions + +*_Major enhancements_* : Morphed the AddressBook4 to Budgeteer. + +** `Reason for enhancement`: To enable Budgeteer to keep track of the financial income and expenses of the person. +** `Highlights`: Time taken to read all 10 kLoc to understand and modify the code. +** `My contributions`: Re-factored functions together with variables across classes to make the components compatible and consistent for the application. +Managed to restructure some parts to make it more cohesive. + +*_Major Enhancement_*: Generation of report of entries in Budgeteer + +** *Function*: To provide the user with a report highlighting their total expenditure as well as income, the user is +able to specify a date range to generate a report for only the entries that fall within the specified date range. + +** *Highlights*: Allows the user to have an overview of their total expenditure for the user to have a better idea of +the percentage of their expenses as well as income. + +** *Justification*: Even though the application allows the user to add details of their expenses and income, there is +no way for the users to get any insights of their expenditure and income. The report command thus provides the users a +platform which allows them to obtain basic information of the percentage of their spending versus income for the +specified time period + +*_Major Enhancement_*: Added optional insights for the report generation feature + +** *Function*: Allows the user to generate a more detailed report with additional insights. +** *Highlights*: This feature improves the utility of the report feature by providing additional insights for the +composition of the expenses and income for the user. + +** *Justification*: It allows the user to have additional information such as the composition of the income and +expenses of the entries in the specified date period, and is categorised by the tags as set by the user. + + +*_Minor Enhancement_#1*: Improved responsiveness of the Cryptocurrencies functions + +** *Function*: Allows the commands to get the amount of purchasable cryptocurrncies to be executed instantaneously. +** *Highlights*: Improved the interactiveness of the app as there was a significant time lag due to the HTTP calls. +This is done by fetching the prices only once per 15 minutes and storing them locally, which allows the prices to be +fetched internally which allows the app to be more responsive. + +** *Justification*: This enhancement has been implemented with the aim of improving interactivity and responsiveness +of the application. + + +=== *Other Contributions* + +** [https://nuscs2113-ay1819s2.github.io/dashboard-beta/#search=yushao2&sort=displayName&since=2019-02-10&until=2019-04-12&timeframe=day&reverse=false&repoSort=true] + +* *_Project management_* : +** Raised milestones and organized the group to complete each milestones before deadline. +** Ensured that milestone objectives were met and submitted to relevant PRs to module repo +** Pushed jar releases onto the team repo. +** Reviewed PRs to ensure that the incoming changes are coherent with the current upstream build. + +* *_Documentation_* : +** Did cosmetic tweaks to existing contents of the User Guide and Developer Guide. +** Proof-read, organised documentation to maintain its coherence. + +* *_Testing_*: +** Modified a few test cases in order to ensure that it is still relevant to the morphed end product. + +* *_Tools_* : +** Integrated a third party library to the project and the team repo + +* *_Debugging team members code_* : +** Modified most of the team members' codes in order to pass the TravisCI build process. +** Modified team members' codes to ensure that the style of implementation is the same in the project. + +* *_Team Management_* : +** Coordinated with Dominic with the sourcing of venues for some of the team meetings +** Ensured that team members meet feature deadlines set within the team + +== Contributions to the User Guide + +|=== +|_Given below are sections I contributed to the <<../UserGuide, User Guide>>. They showcase my ability to write documentation targeting end-users._ +|=== + +include::../UserGuide.adoc[tag=report] +include::../UserGuide.adoc[tag=report_insight] + + + +== Contributions to the Developer Guide + +|=== +|_Given below are sections I contributed to the Developer Guide. They showcase my ability to write technical documentation and the technical depth of my contributions to the project._ +|=== + +include::../DeveloperGuide.adoc[tag=report] diff --git a/docs/templates/_header.html.slim b/docs/templates/_header.html.slim index 1995d26a1615..c596ecea643f 100644 --- a/docs/templates/_header.html.slim +++ b/docs/templates/_header.html.slim @@ -1,26 +1,4 @@ / NOTE: You must restart the gradle daemon after modifying any template file for the changes to take effect. -- if !(attr? 'no-site-header') && (attr? 'site-seedu') - #seedu-header - nav.navbar.navbar-lg.navbar-light.bg-lighter - .container - a.navbar-brand href='https://se-edu.github.io/' - img src=(site_url 'images/SeEduLogo.png') alt='SE-EDU' - ul.navbar-nav - li.nav-item - a.nav-link href='https://se-edu.github.io/addressbook-level1' AB-1 - li.nav-item - a.nav-link href='https://se-edu.github.io/addressbook-level2' AB-2 - li.nav-item - a.nav-link href='https://se-edu.github.io/addressbook-level3' AB-3 - li.nav-item - a.nav-link.active href=(site_url 'index.html') AB-4 - li.nav-item - a.nav-link href='https://se-edu.github.io/collate' Collate - li.nav-item - a.nav-link href='https://se-edu.github.io/se-book' Book - li.nav-item - a.nav-link href='https://se-edu.github.io/learningresources' Resources - - if !(attr? 'no-site-header') #site-header nav.navbar.navbar-light.bg-light @@ -32,9 +10,6 @@ =nav_link('UserGuide', 'UserGuide.html', 'User Guide') li.nav-item =nav_link('DeveloperGuide', 'DeveloperGuide.html', 'Developer Guide') - - if attr? 'site-seedu' - li.nav-item - =nav_link('LearningOutcomes', 'LearningOutcomes.html', 'LOs') li.nav-item =nav_link('AboutUs', 'AboutUs.html', 'About Us') li.nav-item diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2d80b69a7665..58986fb272e0 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ +#Tue Apr 09 12:39:09 SGT 2019 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.8.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.8.1-all.zip 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 1deb3a1e4695..000000000000 --- 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/commons/util/FileUtil.java b/src/main/java/seedu/address/commons/util/FileUtil.java deleted file mode 100644 index b1e2767cdd92..000000000000 --- a/src/main/java/seedu/address/commons/util/FileUtil.java +++ /dev/null @@ -1,83 +0,0 @@ -package seedu.address.commons.util; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.InvalidPathException; -import java.nio.file.Path; -import java.nio.file.Paths; - -/** - * Writes and reads files - */ -public class FileUtil { - - private static final String CHARSET = "UTF-8"; - - public static boolean isFileExists(Path file) { - return Files.exists(file) && Files.isRegularFile(file); - } - - /** - * Returns true if {@code path} can be converted into a {@code Path} via {@link Paths#get(String)}, - * otherwise returns false. - * @param path A string representing the file path. Cannot be null. - */ - public static boolean isValidPath(String path) { - try { - Paths.get(path); - } catch (InvalidPathException ipe) { - return false; - } - return true; - } - - /** - * Creates a file if it does not exist along with its missing parent directories. - * @throws IOException if the file or directory cannot be created. - */ - public static void createIfMissing(Path file) throws IOException { - if (!isFileExists(file)) { - createFile(file); - } - } - - /** - * Creates a file if it does not exist along with its missing parent directories. - */ - public static void createFile(Path file) throws IOException { - if (Files.exists(file)) { - return; - } - - createParentDirsOfFile(file); - - Files.createFile(file); - } - - /** - * Creates parent directories of file if it has a parent directory - */ - public static void createParentDirsOfFile(Path file) throws IOException { - Path parentDir = file.getParent(); - - if (parentDir != null) { - Files.createDirectories(parentDir); - } - } - - /** - * Assumes file exists - */ - public static String readFromFile(Path file) throws IOException { - return new String(Files.readAllBytes(file), CHARSET); - } - - /** - * Writes given string to a file. - * Will create the file if it does not exist yet. - */ - public static void writeToFile(Path file, String content) throws IOException { - Files.write(file, content.getBytes(CHARSET)); - } - -} 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 5cb24a617beb..000000000000 --- a/src/main/java/seedu/address/logic/LogicManager.java +++ /dev/null @@ -1,108 +0,0 @@ -package seedu.address.logic; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.logging.Logger; - -import javafx.beans.property.ReadOnlyProperty; -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 CommandHistory history; - private final AddressBookParser addressBookParser; - private boolean addressBookModified; - - public LogicManager(Model model, Storage storage) { - this.model = model; - this.storage = storage; - history = new CommandHistory(); - addressBookParser = new AddressBookParser(); - - // Set addressBookModified to true whenever the models' address book is modified. - model.getAddressBook().addListener(observable -> addressBookModified = true); - } - - @Override - public CommandResult execute(String commandText) throws CommandException, ParseException { - logger.info("----------------[USER COMMAND][" + commandText + "]"); - addressBookModified = false; - - CommandResult commandResult; - try { - Command command = addressBookParser.parseCommand(commandText); - commandResult = command.execute(model, history); - } finally { - history.add(commandText); - } - - if (addressBookModified) { - logger.info("Address book modified, saving to file."); - 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 ObservableList getHistory() { - return history.getHistory(); - } - - @Override - public Path getAddressBookFilePath() { - return model.getAddressBookFilePath(); - } - - @Override - public GuiSettings getGuiSettings() { - return model.getGuiSettings(); - } - - @Override - public void setGuiSettings(GuiSettings guiSettings) { - model.setGuiSettings(guiSettings); - } - - @Override - public ReadOnlyProperty selectedPersonProperty() { - return model.selectedPersonProperty(); - } - - @Override - public void setSelectedPerson(Person person) { - model.setSelectedPerson(person); - } -} 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 d88e831ff1ce..000000000000 --- a/src/main/java/seedu/address/logic/commands/AddCommand.java +++ /dev/null @@ -1,69 +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.CommandHistory; -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, CommandHistory history) throws CommandException { - requireNonNull(model); - - if (model.hasPerson(toAdd)) { - throw new CommandException(MESSAGE_DUPLICATE_PERSON); - } - - model.addPerson(toAdd); - model.commitAddressBook(); - 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/EditCommand.java b/src/main/java/seedu/address/logic/commands/EditCommand.java deleted file mode 100644 index 952a9e7e7f2b..000000000000 --- a/src/main/java/seedu/address/logic/commands/EditCommand.java +++ /dev/null @@ -1,228 +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.CommandHistory; -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, CommandHistory history) 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); - model.commitAddressBook(); - 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/ListCommand.java b/src/main/java/seedu/address/logic/commands/ListCommand.java deleted file mode 100644 index 6d44824c7d1b..000000000000 --- a/src/main/java/seedu/address/logic/commands/ListCommand.java +++ /dev/null @@ -1,25 +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.logic.CommandHistory; -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, CommandHistory history) { - requireNonNull(model); - model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); - return new CommandResult(MESSAGE_SUCCESS); - } -} 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 b7d57f5db86a..000000000000 --- a/src/main/java/seedu/address/logic/parser/AddressBookParser.java +++ /dev/null @@ -1,92 +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.HistoryCommand; -import seedu.address.logic.commands.ListCommand; -import seedu.address.logic.commands.RedoCommand; -import seedu.address.logic.commands.SelectCommand; -import seedu.address.logic.commands.UndoCommand; -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 SelectCommand.COMMAND_WORD: - return new SelectCommandParser().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 HistoryCommand.COMMAND_WORD: - return new HistoryCommand(); - - case ExitCommand.COMMAND_WORD: - return new ExitCommand(); - - case HelpCommand.COMMAND_WORD: - return new HelpCommand(); - - case UndoCommand.COMMAND_WORD: - return new UndoCommand(); - - case RedoCommand.COMMAND_WORD: - return new RedoCommand(); - - 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 75b1a9bf1190..000000000000 --- 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/model/AddressBook.java b/src/main/java/seedu/address/model/AddressBook.java deleted file mode 100644 index 30557cf81ee7..000000000000 --- a/src/main/java/seedu/address/model/AddressBook.java +++ /dev/null @@ -1,144 +0,0 @@ -package seedu.address.model; - -import static java.util.Objects.requireNonNull; - -import java.util.List; - -import javafx.beans.InvalidationListener; -import javafx.collections.ObservableList; -import seedu.address.commons.util.InvalidationListenerManager; -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; - private final InvalidationListenerManager invalidationListenerManager = new InvalidationListenerManager(); - - /* - * The 'unusual' code block below is an 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); - indicateModified(); - } - - /** - * 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); - indicateModified(); - } - - /** - * 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); - indicateModified(); - } - - /** - * Removes {@code key} from this {@code AddressBook}. - * {@code key} must exist in the address book. - */ - public void removePerson(Person key) { - persons.remove(key); - indicateModified(); - } - - @Override - public void addListener(InvalidationListener listener) { - invalidationListenerManager.addListener(listener); - } - - @Override - public void removeListener(InvalidationListener listener) { - invalidationListenerManager.removeListener(listener); - } - - /** - * Notifies listeners that the address book has been modified. - */ - protected void indicateModified() { - invalidationListenerManager.callListeners(this); - } - - //// 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 e857533821b6..000000000000 --- a/src/main/java/seedu/address/model/Model.java +++ /dev/null @@ -1,130 +0,0 @@ -package seedu.address.model; - -import java.nio.file.Path; -import java.util.function.Predicate; - -import javafx.beans.property.ReadOnlyProperty; -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); - - /** - * Returns true if the model has previous address book states to restore. - */ - boolean canUndoAddressBook(); - - /** - * Returns true if the model has undone address book states to restore. - */ - boolean canRedoAddressBook(); - - /** - * Restores the model's address book to its previous state. - */ - void undoAddressBook(); - - /** - * Restores the model's address book to its previously undone state. - */ - void redoAddressBook(); - - /** - * Saves the current address book state for undo/redo. - */ - void commitAddressBook(); - - /** - * Selected person in the filtered person list. - * null if no person is selected. - */ - ReadOnlyProperty selectedPersonProperty(); - - /** - * Returns the selected person in the filtered person list. - * null if no person is selected. - */ - Person getSelectedPerson(); - - /** - * Sets the selected person in the filtered person list. - */ - void setSelectedPerson(Person person); -} 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 b56806232814..000000000000 --- a/src/main/java/seedu/address/model/ModelManager.java +++ /dev/null @@ -1,235 +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.Objects; -import java.util.function.Predicate; -import java.util.logging.Logger; - -import javafx.beans.property.ReadOnlyProperty; -import javafx.beans.property.SimpleObjectProperty; -import javafx.collections.ListChangeListener; -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; -import seedu.address.model.person.exceptions.PersonNotFoundException; - -/** - * 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 VersionedAddressBook versionedAddressBook; - private final UserPrefs userPrefs; - private final FilteredList filteredPersons; - private final SimpleObjectProperty selectedPerson = new SimpleObjectProperty<>(); - - /** - * Initializes a ModelManager with the given addressBook and userPrefs. - */ - public ModelManager(ReadOnlyAddressBook addressBook, ReadOnlyUserPrefs userPrefs) { - super(); - requireAllNonNull(addressBook, userPrefs); - - logger.fine("Initializing with address book: " + addressBook + " and user prefs " + userPrefs); - - versionedAddressBook = new VersionedAddressBook(addressBook); - this.userPrefs = new UserPrefs(userPrefs); - filteredPersons = new FilteredList<>(versionedAddressBook.getPersonList()); - filteredPersons.addListener(this::ensureSelectedPersonIsValid); - } - - 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) { - versionedAddressBook.resetData(addressBook); - } - - @Override - public ReadOnlyAddressBook getAddressBook() { - return versionedAddressBook; - } - - @Override - public boolean hasPerson(Person person) { - requireNonNull(person); - return versionedAddressBook.hasPerson(person); - } - - @Override - public void deletePerson(Person target) { - versionedAddressBook.removePerson(target); - } - - @Override - public void addPerson(Person person) { - versionedAddressBook.addPerson(person); - updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); - } - - @Override - public void setPerson(Person target, Person editedPerson) { - requireAllNonNull(target, editedPerson); - - versionedAddressBook.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); - } - - //=========== Undo/Redo ================================================================================= - - @Override - public boolean canUndoAddressBook() { - return versionedAddressBook.canUndo(); - } - - @Override - public boolean canRedoAddressBook() { - return versionedAddressBook.canRedo(); - } - - @Override - public void undoAddressBook() { - versionedAddressBook.undo(); - } - - @Override - public void redoAddressBook() { - versionedAddressBook.redo(); - } - - @Override - public void commitAddressBook() { - versionedAddressBook.commit(); - } - - //=========== Selected person =========================================================================== - - @Override - public ReadOnlyProperty selectedPersonProperty() { - return selectedPerson; - } - - @Override - public Person getSelectedPerson() { - return selectedPerson.getValue(); - } - - @Override - public void setSelectedPerson(Person person) { - if (person != null && !filteredPersons.contains(person)) { - throw new PersonNotFoundException(); - } - selectedPerson.setValue(person); - } - - /** - * Ensures {@code selectedPerson} is a valid person in {@code filteredPersons}. - */ - private void ensureSelectedPersonIsValid(ListChangeListener.Change change) { - while (change.next()) { - if (selectedPerson.getValue() == null) { - // null is always a valid selected person, so we do not need to check that it is valid anymore. - return; - } - - boolean wasSelectedPersonReplaced = change.wasReplaced() && change.getAddedSize() == change.getRemovedSize() - && change.getRemoved().contains(selectedPerson.getValue()); - if (wasSelectedPersonReplaced) { - // Update selectedPerson to its new value. - int index = change.getRemoved().indexOf(selectedPerson.getValue()); - selectedPerson.setValue(change.getAddedSubList().get(index)); - continue; - } - - boolean wasSelectedPersonRemoved = change.getRemoved().stream() - .anyMatch(removedPerson -> selectedPerson.getValue().isSamePerson(removedPerson)); - if (wasSelectedPersonRemoved) { - // Select the person that came before it in the list, - // or clear the selection if there is no such person. - selectedPerson.setValue(change.getFrom() > 0 ? change.getList().get(change.getFrom() - 1) : null); - } - } - } - - @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 versionedAddressBook.equals(other.versionedAddressBook) - && userPrefs.equals(other.userPrefs) - && filteredPersons.equals(other.filteredPersons) - && Objects.equals(selectedPerson.get(), other.selectedPerson.get()); - } - -} 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 60472ca22a09..000000000000 --- 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 a5bbe0b6a5fc..000000000000 --- a/src/main/java/seedu/address/model/person/Email.java +++ /dev/null @@ -1,67 +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 + ") .\n" - + "2. This is followed by a '@' and then a domain name. " - + "The domain name must:\n" - + " - be at least 2 characters long\n" - + " - start and end with alphanumeric characters\n" - + " - consist of alphanumeric characters, a period or a hyphen for the characters in between, if any."; - // alphanumeric and special characters - private static final String LOCAL_PART_REGEX = "^[\\w" + SPECIAL_CHARACTERS + "]+"; - private static final String DOMAIN_FIRST_CHARACTER_REGEX = "[^\\W_]"; // alphanumeric characters except underscore - private static final String DOMAIN_MIDDLE_REGEX = "[a-zA-Z0-9.-]*"; // alphanumeric, period and hyphen - private static final String DOMAIN_LAST_CHARACTER_REGEX = "[^\\W_]$"; - public static final String VALIDATION_REGEX = LOCAL_PART_REGEX + "@" - + DOMAIN_FIRST_CHARACTER_REGEX + DOMAIN_MIDDLE_REGEX + DOMAIN_LAST_CHARACTER_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/Person.java b/src/main/java/seedu/address/model/person/Person.java deleted file mode 100644 index 557a7a60cd51..000000000000 --- a/src/main/java/seedu/address/model/person/Person.java +++ /dev/null @@ -1,120 +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 of the same name have at least one other identity field that is the same. - * 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()) - && (otherPerson.getPhone().equals(getPhone()) || otherPerson.getEmail().equals(getEmail())); - } - - /** - * 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()) - .append(" Tags: "); - getTags().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 872c76b382fd..000000000000 --- 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 0fee4fe57e6b..000000000000 --- 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 d7290f594423..000000000000 --- 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 fa764426ca73..000000000000 --- 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/util/SampleDataUtil.java b/src/main/java/seedu/address/model/util/SampleDataUtil.java deleted file mode 100644 index 1806da4facfa..000000000000 --- 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/JsonAdaptedPerson.java b/src/main/java/seedu/address/storage/JsonAdaptedPerson.java deleted file mode 100644 index a6321cec2eac..000000000000 --- 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/JsonSerializableAddressBook.java b/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java deleted file mode 100644 index 5efd834091d4..000000000000 --- 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/ui/PersonListPanel.java b/src/main/java/seedu/address/ui/PersonListPanel.java deleted file mode 100644 index 5ca3fa4fc671..000000000000 --- a/src/main/java/seedu/address/ui/PersonListPanel.java +++ /dev/null @@ -1,71 +0,0 @@ -package seedu.address.ui; - -import java.util.Objects; -import java.util.function.Consumer; -import java.util.logging.Logger; - -import javafx.beans.value.ObservableValue; -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; - - public PersonListPanel(ObservableList personList, ObservableValue selectedPerson, - Consumer onSelectedPersonChange) { - super(FXML); - personListView.setItems(personList); - personListView.setCellFactory(listView -> new PersonListViewCell()); - personListView.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> { - logger.fine("Selection in person list panel changed to : '" + newValue + "'"); - onSelectedPersonChange.accept(newValue); - }); - selectedPerson.addListener((observable, oldValue, newValue) -> { - logger.fine("Selected person changed to: " + newValue); - - // Don't modify selection if we are already selecting the selected person, - // otherwise we would have an infinite loop. - if (Objects.equals(personListView.getSelectionModel().getSelectedItem(), newValue)) { - return; - } - - if (newValue == null) { - personListView.getSelectionModel().clearSelection(); - } else { - int index = personListView.getItems().indexOf(newValue); - personListView.scrollTo(index); - personListView.getSelectionModel().clearAndSelect(index); - } - }); - } - - /** - * 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/java/seedu/address/AppParameters.java b/src/main/java/seedu/budgeteer/AppParameters.java similarity index 93% rename from src/main/java/seedu/address/AppParameters.java rename to src/main/java/seedu/budgeteer/AppParameters.java index ab552c398f3d..2d0fdfa0f512 100644 --- a/src/main/java/seedu/address/AppParameters.java +++ b/src/main/java/seedu/budgeteer/AppParameters.java @@ -1,4 +1,4 @@ -package seedu.address; +package seedu.budgeteer; import java.nio.file.Path; import java.nio.file.Paths; @@ -7,8 +7,8 @@ import java.util.logging.Logger; import javafx.application.Application; -import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.util.FileUtil; +import seedu.budgeteer.commons.core.LogsCenter; +import seedu.budgeteer.commons.util.FileUtil; /** * Represents the parsed command-line parameters given to the application. diff --git a/src/main/java/seedu/address/MainApp.java b/src/main/java/seedu/budgeteer/MainApp.java similarity index 64% rename from src/main/java/seedu/address/MainApp.java rename to src/main/java/seedu/budgeteer/MainApp.java index a92d4d5d71f0..ca0e3337d6cc 100644 --- a/src/main/java/seedu/address/MainApp.java +++ b/src/main/java/seedu/budgeteer/MainApp.java @@ -1,5 +1,6 @@ -package seedu.address; +package seedu.budgeteer; +import java.io.File; import java.io.IOException; import java.nio.file.Path; import java.util.Optional; @@ -7,33 +8,40 @@ 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; + +import seedu.budgeteer.commons.core.Config; +import seedu.budgeteer.commons.core.LogsCenter; +import seedu.budgeteer.commons.core.Version; +import seedu.budgeteer.commons.exceptions.DataConversionException; +import seedu.budgeteer.commons.util.ConfigUtil; +import seedu.budgeteer.commons.util.CryptoUtil; +import seedu.budgeteer.commons.util.EncryptionUtil; +import seedu.budgeteer.commons.util.FileUtil; +import seedu.budgeteer.commons.util.StringUtil; +import seedu.budgeteer.logic.Logic; +import seedu.budgeteer.logic.LogicManager; +import seedu.budgeteer.model.EntriesBook; +import seedu.budgeteer.model.Model; +import seedu.budgeteer.model.ModelManager; +import seedu.budgeteer.model.ReadOnlyEntriesBook; +import seedu.budgeteer.model.ReadOnlyUserPrefs; +import seedu.budgeteer.model.UserPrefs; +import seedu.budgeteer.model.util.SampleDataUtil; +import seedu.budgeteer.storage.BudgeteerStorage; +import seedu.budgeteer.storage.JsonBudgeteerStorage; +import seedu.budgeteer.storage.JsonUserPrefsStorage; +import seedu.budgeteer.storage.Storage; +import seedu.budgeteer.storage.StorageManager; +import seedu.budgeteer.storage.UserPrefsStorage; + +import seedu.budgeteer.ui.Ui; +import seedu.budgeteer.ui.UiManager; /** * The main entry point to the application. */ + + public class MainApp extends Application { public static final Version VERSION = new Version(0, 6, 0, true); @@ -48,7 +56,7 @@ public class MainApp extends Application { @Override public void init() throws Exception { - logger.info("=============================[ Initializing AddressBook ]==========================="); + logger.info("=============================[ Initializing EntriesBook ]==========================="); super.init(); AppParameters appParameters = AppParameters.parse(getParameters()); @@ -56,8 +64,10 @@ 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); + BudgeteerStorage budgeteerStorage = new JsonBudgeteerStorage(userPrefs.getAddressBookFilePath()); + storage = new StorageManager(budgeteerStorage, userPrefsStorage); + + CryptoUtil.getInstance(); //Initialize the crypto prices initLogging(config); @@ -69,25 +79,36 @@ public void init() throws Exception { } /** - * 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 budgeteer book and {@code userPrefs}.
+ * The data from the sample budgeteer book will be used instead if {@code storage}'s budgeteer book is not found, + * or an empty budgeteer book will be used instead if errors occur when reading {@code storage}'s budgeteer book. */ private Model initModelManager(Storage storage, ReadOnlyUserPrefs userPrefs) { - Optional addressBookOptional; - ReadOnlyAddressBook initialData; + Optional entrieskOptional; + ReadOnlyEntriesBook initialData; + + File file = new File(userPrefs.getPasswordFilePath()); + try { + if (FileUtil.isPassExists(file)) { + File book = new File(String.valueOf(userPrefs.getAddressBookFilePath())); + EncryptionUtil.decrypt(book); + } + } catch (IOException e) { + logger.warning("Problem while reading from the password file"); + } + try { - addressBookOptional = storage.readAddressBook(); - if (!addressBookOptional.isPresent()) { - logger.info("Data file not found. Will be starting with a sample AddressBook"); + entrieskOptional = storage.readEntriesBook(); + if (!entrieskOptional.isPresent()) { + logger.info("Data file not found. Will be starting with a sample EntriesBook"); } - initialData = addressBookOptional.orElseGet(SampleDataUtil::getSampleAddressBook); + initialData = entrieskOptional.orElseGet(SampleDataUtil::getSampleAddressBook); } 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 EntriesBook"); + initialData = new EntriesBook(); } 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 EntriesBook"); + initialData = new EntriesBook(); } return new ModelManager(initialData, userPrefs); @@ -151,7 +172,7 @@ 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 EntriesBook"); initializedPrefs = new UserPrefs(); } @@ -167,7 +188,7 @@ protected UserPrefs initPrefs(UserPrefsStorage storage) { @Override public void start(Stage primaryStage) { - logger.info("Starting AddressBook " + MainApp.VERSION); + logger.info("Starting EntriesBook " + MainApp.VERSION); ui.start(primaryStage); } diff --git a/src/main/java/seedu/address/commons/core/Config.java b/src/main/java/seedu/budgeteer/commons/core/Config.java similarity index 97% rename from src/main/java/seedu/address/commons/core/Config.java rename to src/main/java/seedu/budgeteer/commons/core/Config.java index 911457455217..18d469c246b3 100644 --- a/src/main/java/seedu/address/commons/core/Config.java +++ b/src/main/java/seedu/budgeteer/commons/core/Config.java @@ -1,4 +1,4 @@ -package seedu.address.commons.core; +package seedu.budgeteer.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/seedu/budgeteer/commons/core/GuiSettings.java similarity index 98% rename from src/main/java/seedu/address/commons/core/GuiSettings.java rename to src/main/java/seedu/budgeteer/commons/core/GuiSettings.java index 5ace559ad156..d471cf3b6cbc 100644 --- a/src/main/java/seedu/address/commons/core/GuiSettings.java +++ b/src/main/java/seedu/budgeteer/commons/core/GuiSettings.java @@ -1,4 +1,4 @@ -package seedu.address.commons.core; +package seedu.budgeteer.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/seedu/budgeteer/commons/core/LogsCenter.java similarity index 99% rename from src/main/java/seedu/address/commons/core/LogsCenter.java rename to src/main/java/seedu/budgeteer/commons/core/LogsCenter.java index 431e7185e762..503d5b068b36 100644 --- a/src/main/java/seedu/address/commons/core/LogsCenter.java +++ b/src/main/java/seedu/budgeteer/commons/core/LogsCenter.java @@ -1,4 +1,4 @@ -package seedu.address.commons.core; +package seedu.budgeteer.commons.core; import java.io.IOException; import java.util.Arrays; diff --git a/src/main/java/seedu/budgeteer/commons/core/Messages.java b/src/main/java/seedu/budgeteer/commons/core/Messages.java new file mode 100644 index 000000000000..ba63190cb21e --- /dev/null +++ b/src/main/java/seedu/budgeteer/commons/core/Messages.java @@ -0,0 +1,38 @@ +package seedu.budgeteer.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_ENTRY_DISPLAYED_INDEX = "The entry index provided is invalid"; + public static final String MESSAGE_ENTRYS_LISTED_OVERVIEW = "%1$d entry/s listed!"; + public static final String MESSAGE_INVALID_TAG = "Tag Not Found in Budgeter"; + public static final String MESSAGE_UNREALISTIC_DIRECTORY = "Please choose existing directory/file path.\n"; + public static final String MESSAGE_EXCEL_FILE_WRITTEN_SUCCESSFULLY = + "The Excel file has been written successfully in path: %2$s.\n"; + public static final String MESSAGE_EXPORT_COMMAND_ERRORS = "There is error to export.\n"; + + public static final String MESSAGE_ARCHIVE_COMMAND_ERRORS = "There is error to archive.\n"; + + public static final String MESSAGE_IMPORT_COMMAND_ERRORS = "There is no entry found.\n"; + + public static final String MESSAGE_ARCHIVE_SUCCESSFULLY = + " The records in the Excel file will be no longer in the current Budgeter.\n"; + public static final String MESSAGE_INVALID_DATE_REQUIRED = + "Please enter exact TWO Dates, Start_Date and End_Date.\n"; + public static final String MESSAGE_INVALID_STARTDATE_ENDDATE = + "Please enter the Start_Date smaller than or equal to the End_Date.\n"; + public static final String MESSAGE_INVALID_ENTRY_EXCEL_FILE = + "The cells for Name, Date, Money Received/Spent, Tags should be in correct order." + + " The Cell should only be String or Numeric type." + + " The first row of your table should come with 4 columns, namely, " + + "NAME, DATE, MONEY SPENT/RECEIVED and TAGS (case in-sensitive).\n"; + public static final String MESSAGE_RECORD_ADDED_SUCCESSFULLY = + "All records from the %1$s are read" + + " and only non-existing records are added to the current Budgeteer.\n"; + + +} diff --git a/src/main/java/seedu/address/commons/core/Version.java b/src/main/java/seedu/budgeteer/commons/core/Version.java similarity index 96% rename from src/main/java/seedu/address/commons/core/Version.java rename to src/main/java/seedu/budgeteer/commons/core/Version.java index e117f91b3b2e..4ff05b56cfae 100644 --- a/src/main/java/seedu/address/commons/core/Version.java +++ b/src/main/java/seedu/budgeteer/commons/core/Version.java @@ -1,4 +1,4 @@ -package seedu.address.commons.core; +package seedu.budgeteer.commons.core; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -61,7 +61,7 @@ public static Version fromString(String versionString) throws IllegalArgumentExc return new Version(Integer.parseInt(versionMatcher.group(1)), Integer.parseInt(versionMatcher.group(2)), Integer.parseInt(versionMatcher.group(3)), - versionMatcher.group(4) == null ? false : true); + versionMatcher.group(4) != null); } @JsonValue diff --git a/src/main/java/seedu/address/commons/core/index/Index.java b/src/main/java/seedu/budgeteer/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/seedu/budgeteer/commons/core/index/Index.java index 19536439c099..4979642fc8cd 100644 --- a/src/main/java/seedu/address/commons/core/index/Index.java +++ b/src/main/java/seedu/budgeteer/commons/core/index/Index.java @@ -1,4 +1,4 @@ -package seedu.address.commons.core.index; +package seedu.budgeteer.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/seedu/budgeteer/commons/exceptions/DataConversionException.java similarity index 83% rename from src/main/java/seedu/address/commons/exceptions/DataConversionException.java rename to src/main/java/seedu/budgeteer/commons/exceptions/DataConversionException.java index 1f689bd8e3f9..426b2ed0f9ea 100644 --- a/src/main/java/seedu/address/commons/exceptions/DataConversionException.java +++ b/src/main/java/seedu/budgeteer/commons/exceptions/DataConversionException.java @@ -1,4 +1,4 @@ -package seedu.address.commons.exceptions; +package seedu.budgeteer.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/seedu/budgeteer/commons/exceptions/IllegalValueException.java similarity index 92% rename from src/main/java/seedu/address/commons/exceptions/IllegalValueException.java rename to src/main/java/seedu/budgeteer/commons/exceptions/IllegalValueException.java index 19124db485c9..33608291cc02 100644 --- a/src/main/java/seedu/address/commons/exceptions/IllegalValueException.java +++ b/src/main/java/seedu/budgeteer/commons/exceptions/IllegalValueException.java @@ -1,4 +1,4 @@ -package seedu.address.commons.exceptions; +package seedu.budgeteer.commons.exceptions; /** * Signals that some given data does not fulfill some constraints. diff --git a/src/main/java/seedu/budgeteer/commons/exceptions/WrongPasswordException.java b/src/main/java/seedu/budgeteer/commons/exceptions/WrongPasswordException.java new file mode 100644 index 000000000000..8f7be53c67c3 --- /dev/null +++ b/src/main/java/seedu/budgeteer/commons/exceptions/WrongPasswordException.java @@ -0,0 +1,20 @@ +package seedu.budgeteer.commons.exceptions; + +/** + * Signals that the password provided is wrong. + */ +public class WrongPasswordException extends Exception { + + private static final String MESSAGE_WRONG_PASSWORD = "Wrong Password"; + + public WrongPasswordException() { + super(MESSAGE_WRONG_PASSWORD); + } + + /** + * @param cause of the main exception + */ + public WrongPasswordException(Throwable cause) { + super(MESSAGE_WRONG_PASSWORD, cause); + } +} diff --git a/src/main/java/seedu/budgeteer/commons/model/AddressBookChangedEvent.java b/src/main/java/seedu/budgeteer/commons/model/AddressBookChangedEvent.java new file mode 100755 index 000000000000..a7a783908214 --- /dev/null +++ b/src/main/java/seedu/budgeteer/commons/model/AddressBookChangedEvent.java @@ -0,0 +1,18 @@ +package seedu.budgeteer.commons.model; + +import seedu.budgeteer.model.ReadOnlyEntriesBook; + +/** Indicates the EntriesBook in the model has changed*/ +public class AddressBookChangedEvent extends BaseEvent { + + public final ReadOnlyEntriesBook data; + + public AddressBookChangedEvent(ReadOnlyEntriesBook data) { + this.data = data; + } + + @Override + public String toString() { + return "number of records " + data.getEntryList().size(); + } +} diff --git a/src/main/java/seedu/budgeteer/commons/model/BaseEvent.java b/src/main/java/seedu/budgeteer/commons/model/BaseEvent.java new file mode 100755 index 000000000000..1613fcceb73a --- /dev/null +++ b/src/main/java/seedu/budgeteer/commons/model/BaseEvent.java @@ -0,0 +1,16 @@ +package seedu.budgeteer.commons.model; + +/** + * The base class for all event classes. + */ +public abstract class BaseEvent { + + /** + * All Events should have a clear unambiguous custom toString message so that feedback message creation + * stays consistent and reusable. + * + * For example, the event manager post method will call any posted event's toString and print it in the console. + */ + public abstract String toString(); + +} diff --git a/src/main/java/seedu/address/commons/util/AppUtil.java b/src/main/java/seedu/budgeteer/commons/util/AppUtil.java similarity index 93% rename from src/main/java/seedu/address/commons/util/AppUtil.java rename to src/main/java/seedu/budgeteer/commons/util/AppUtil.java index da90201dfd64..4cd907fc5e79 100644 --- a/src/main/java/seedu/address/commons/util/AppUtil.java +++ b/src/main/java/seedu/budgeteer/commons/util/AppUtil.java @@ -1,9 +1,9 @@ -package seedu.address.commons.util; +package seedu.budgeteer.commons.util; import static java.util.Objects.requireNonNull; import javafx.scene.image.Image; -import seedu.address.MainApp; +import seedu.budgeteer.MainApp; /** * A container for App specific utility functions diff --git a/src/main/java/seedu/address/commons/util/CollectionUtil.java b/src/main/java/seedu/budgeteer/commons/util/CollectionUtil.java similarity index 96% rename from src/main/java/seedu/address/commons/util/CollectionUtil.java rename to src/main/java/seedu/budgeteer/commons/util/CollectionUtil.java index eafe4dfd6818..19b9825b08c9 100644 --- a/src/main/java/seedu/address/commons/util/CollectionUtil.java +++ b/src/main/java/seedu/budgeteer/commons/util/CollectionUtil.java @@ -1,4 +1,4 @@ -package seedu.address.commons.util; +package seedu.budgeteer.commons.util; import static java.util.Objects.requireNonNull; @@ -32,4 +32,5 @@ public static void requireAllNonNull(Collection items) { public static boolean isAnyNonNull(Object... items) { return items != null && Arrays.stream(items).anyMatch(Objects::nonNull); } + } diff --git a/src/main/java/seedu/budgeteer/commons/util/CompareUtil.java b/src/main/java/seedu/budgeteer/commons/util/CompareUtil.java new file mode 100644 index 000000000000..096746e1dce1 --- /dev/null +++ b/src/main/java/seedu/budgeteer/commons/util/CompareUtil.java @@ -0,0 +1,63 @@ +package seedu.budgeteer.commons.util; + +import java.util.Comparator; +import java.util.Set; + +import seedu.budgeteer.model.entry.Date; +import seedu.budgeteer.model.entry.Entry; +import seedu.budgeteer.model.tag.Tag; + + +/** + * Comparator to sort {@code Entry}s by name, date and moneyflow attributes. + */ +public class CompareUtil { + + public static Comparator compareNameAttribute() { + return Comparator.comparing(a -> a.getName().fullName.toLowerCase()); + } + + public static Comparator compareDateAttribute() { + return (a, b) -> compareDate().compare(a.getDate(), b.getDate()); + } + + public static Comparator compareCashflowAttribute() { + return Comparator.comparing(a -> a.getCashFlow().valueDouble); + } + + /** + * This function compares date1 and date2 and returns an integer. + * If date1 is earlier than date2, it returns -1. + * If date1 is later than date2, it returns 1. + * If date1 is equal to date2, it returns 0. + */ + public static Comparator compareDate() { + return (date1, date2) -> { + if (date1.getYear() < date2.getYear()) { + return -1; + } else if (date1.getYear() == date2.getYear()) { + if (date1.getMonth() < date2.getMonth()) { + return -1; + } else if (date1.getMonth() == date2.getMonth()) { + if (date1.getDay() < date2.getDay()) { + return -1; + } else if (date1.getDay() == date2.getDay()) { + return 0; + } else { + return 1; + } + } else { + return 1; + } + } else { + return 1; + } + }; + } + + + public static Comparator> compareTags() { + return Comparator.comparing(Object::toString); + } +} + diff --git a/src/main/java/seedu/address/commons/util/ConfigUtil.java b/src/main/java/seedu/budgeteer/commons/util/ConfigUtil.java similarity index 76% rename from src/main/java/seedu/address/commons/util/ConfigUtil.java rename to src/main/java/seedu/budgeteer/commons/util/ConfigUtil.java index f7f8a2bd44c0..bc8427b4be34 100644 --- a/src/main/java/seedu/address/commons/util/ConfigUtil.java +++ b/src/main/java/seedu/budgeteer/commons/util/ConfigUtil.java @@ -1,11 +1,11 @@ -package seedu.address.commons.util; +package seedu.budgeteer.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 seedu.budgeteer.commons.core.Config; +import seedu.budgeteer.commons.exceptions.DataConversionException; /** * A class for accessing the Config File. diff --git a/src/main/java/seedu/budgeteer/commons/util/CryptoUtil.java b/src/main/java/seedu/budgeteer/commons/util/CryptoUtil.java new file mode 100644 index 000000000000..dbb74d030c94 --- /dev/null +++ b/src/main/java/seedu/budgeteer/commons/util/CryptoUtil.java @@ -0,0 +1,168 @@ +package seedu.budgeteer.commons.util; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Date; + +import seedu.budgeteer.commons.exceptions.IllegalValueException; + +/** + * Utility class to fetch the crypto-currencies prices from the internet using multithreading + * and to store them for fast retrieval. + * Once an interval of time has passed, the prices are deemed to be expired and will be fetched again. + * This reduces the amount of lag due to fetching from the internet. + */ +public class CryptoUtil { + + /** + * A Specialised thread class used for fetching the price of a particular crypto-currency type + * To be used with multi-threading to improve performance for making multiple http calls concurrently + */ + class CryptoThread extends Thread { + private String cryptoType; + + public CryptoThread(String cryptoType) { + this.cryptoType = cryptoType; + } + + /** + * Function to be run on the thread when thread.start(); is called + * Fetches the crypto-price by using an API call via http request. + */ + public void run() { + + try { + + if (this.cryptoType.equalsIgnoreCase("BTC")) { + btcPrice = fetchBtc(); + } else if (this.cryptoType.equalsIgnoreCase("ETH")) { + ethPrice = fetchEth(); + } else if (this.cryptoType.equalsIgnoreCase("LTC")) { + ltcPrice = fetchLtc(); + } else { + throw new IllegalValueException("Unknown Crypto"); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + private static Double btcPrice = 0.0; + private static Double ethPrice = 0.0; + private static Double ltcPrice = 0.0; + private static CryptoUtil cryptoUtilInstance = null; + + private Date lastUpdated; + + private CryptoUtil() { + updateCryptoPrices(); + } + + public static CryptoUtil getInstance() { + if (cryptoUtilInstance == null) { + cryptoUtilInstance = new CryptoUtil(); + } + + return cryptoUtilInstance; + } + + /** + * Update internal prices by using multi-threading. + * Uses join to ensure that all threads run to completion before exiting function. + */ + private void updateCryptoPrices() { + CryptoThread btcThread = new CryptoThread("BTC"); + CryptoThread ethThread = new CryptoThread("ETH"); + CryptoThread ltcThread = new CryptoThread("LTC"); + btcThread.start(); + ethThread.start(); + ltcThread.start(); + try { + btcThread.join(); + ethThread.join(); + ltcThread.join(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + lastUpdated = new Date(); + } + + /** + * Check the interval to determine if 15 minutes have passed or not. + * If the interval has lapsed, update the price data. + */ + private void checkIntervalAndUpdate() { + Date currentTime = new Date(); + long diff = currentTime.getTime() - lastUpdated.getTime(); + long diffMinutes = diff / (60 * 1000); + + if (diffMinutes > 10) { + updateCryptoPrices(); + } + + } + + public double getBtc() { + checkIntervalAndUpdate(); + return btcPrice; + } + + public double getEth() { + checkIntervalAndUpdate(); + return ethPrice; + } + + public double getLtc() { + checkIntervalAndUpdate(); + return ltcPrice; + } + + private double fetchBtc() { + return getCrypto("BTC"); + } + + private double fetchEth() { + return getCrypto("ETH"); + } + + private double fetchLtc() { + return getCrypto("LTC"); + } + + private double getCrypto(String cryptoType) { + double price = 0.0; + try { + URL url = new URL("https://min-api.cryptocompare.com/data/pricemulti?fsyms=" + + cryptoType + "&tsyms=SGD"); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("GET"); + conn.setRequestProperty("Accept", "application/json"); + + if (conn.getResponseCode() != 200) { + throw new RuntimeException("Failed : HTTP error code : " + + conn.getResponseCode()); + } + + BufferedReader br = new BufferedReader(new InputStreamReader((conn.getInputStream()))); + String output = br.readLine(); + output = output.substring(14); + output = output.substring(0, output.length() - 2); + price = Float.parseFloat(output); + + conn.disconnect(); + } catch (MalformedURLException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + + return price; + + } +} diff --git a/src/main/java/seedu/budgeteer/commons/util/DateUtil.java b/src/main/java/seedu/budgeteer/commons/util/DateUtil.java new file mode 100644 index 000000000000..87b740d9bc82 --- /dev/null +++ b/src/main/java/seedu/budgeteer/commons/util/DateUtil.java @@ -0,0 +1,118 @@ +//@@author ngkaicong +package seedu.budgeteer.commons.util; + +import static seedu.budgeteer.commons.util.CompareUtil.compareDate; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import java.util.List; + +import seedu.budgeteer.model.entry.Date; + +/** + * Contains helper methods to determine whether a calendar parameter is logical or not + */ +public class DateUtil { + + private static final List maxDaysInEachMonth = Arrays.asList(31, 28, 31, 30, 31, 30, + 31, 31, 30, 31, 30, 31); + + private static final List maxDaysInEachMonthLeapYear = Arrays.asList(31, 29, 31, 30, 31, 30, + 31, 31, 30, 31, 30, 31); + + /** + * This function checks whether the given day and month falls within the constraints of modern calendars + */ + public static boolean isValidDate(int day, int month, int year) { + if (isLeapYear(year)) { + if (month > maxDaysInEachMonthLeapYear.size()) { + return false; + } + return day <= maxDaysInEachMonthLeapYear.get(month - 1); + } else { + if (month > maxDaysInEachMonth.size()) { + return false; + } + return day <= maxDaysInEachMonth.get(month - 1); + } + } + + /** + * Checks if the year is Leap Year or not. + * @param year + * @return the result whether the year is Leap year. + */ + public static boolean isLeapYear (int year) { + if (year % 400 == 0) { + return true; + } else if (year % 100 == 0) { + return false; + } else { + return year % 4 == 0; + } + } + + /** + * Checks whether a {@code Date} is earlier than another given {@code Date} + * @param date1 + * @param date2 + * @return True if date1 is earlier than date 2and False otherwise + */ + public static boolean isEarlierThan(Date date1, Date date2) { + return compareDate().compare(date1, date2) <= -1; + } + + /** + * Checks whether a {@code Date} is later than another given {@code Date} + * @param date1 + * @param date2 + * @return True if date1 is later than date 2and False otherwise + */ + public static boolean isLaterThan(Date date1, Date date2) { + return compareDate().compare(date1, date2) >= 1; + } + + + /** + * Computes today's date using Java library {@link LocalDate} by processing the date in yyyy-mm-dd into dd-mm-yyyy. + * @return Date + */ + public static Date getDateToday() { + String dateToday = LocalDate.now().toString(); + String[] args = dateToday.split("-"); + return new Date(String.format("%s-%s-%s", args[2], args[1], args[0])); + } + + /** + * Formats a date object into a display complying with + */ + public static String formatDate(Date date) { + LocalDate localDate = LocalDate.of(date.getYear(), date.getMonth(), date.getDay()); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd MMMM yyyy"); + return localDate.format(formatter); + } + + /** + * Computes today's date using Java library {@link LocalDate} by processing the date in yyyy-mm-dd into dd-mm-yyyy + * and returns it as a string form for input. + * @return String date + */ + public static String getDateTodayForInput() { + String dateToday = LocalDate.now().toString(); + String[] args = dateToday.split("-"); + return String.format("%s-%s-%s", args[2], args[1], args[0]); + } + + /** + * Computes today's date using Java library {@link LocalDate} by processing the date in yyyy-mm-dd into dd-mm-yyyy + * and returns it as a string form for input. + * @return String date + */ + public static String getDateYesterdayForInput() { + String dateYesterday = LocalDate.now().minusDays(1).toString(); + String[] args = dateYesterday.split("-"); + return String.format("%s-%s-%s", args[2] , args[1], args[0]); + } +} + diff --git a/src/main/java/seedu/budgeteer/commons/util/EncryptionUtil.java b/src/main/java/seedu/budgeteer/commons/util/EncryptionUtil.java new file mode 100644 index 000000000000..36c8aebf40bd --- /dev/null +++ b/src/main/java/seedu/budgeteer/commons/util/EncryptionUtil.java @@ -0,0 +1,139 @@ +package seedu.budgeteer.commons.util; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.security.GeneralSecurityException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.logging.Logger; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; + +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.SecretKey; + +import javax.crypto.spec.SecretKeySpec; + +import seedu.budgeteer.commons.core.LogsCenter; + +/** + * A Class that encrypts and decrypts XML files stored on the hard disk. + * + */ + +public class EncryptionUtil { + /** + *The standard version of the JRE/JDK are under export restrictions. + *That also includes that some cryptographic algorithms are not allowed to be shipped in the standard version. + *Replace files in library with Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files 8 + */ + + private static final String password = "CS210321CS210321"; + private static final Logger logger = LogsCenter.getLogger(EncryptionUtil.class); + + /** + * Encrypts XML file + * + * @param file path of the file to be encrypted + * @throws IOException if file could not be found + */ + public static void encrypt(File file) throws IOException { + + try { + Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); + SecretKey secretKey = generateKey(); + cipher.init(Cipher.ENCRYPT_MODE, secretKey); + fileToBytes(cipher, file); + } catch (GeneralSecurityException gse) { + logger.severe("Cipher or Padding might not be supported " + gse.getMessage()); + } catch (UnsupportedEncodingException use) { + logger.info("Encoding Unsupported " + use.getMessage()); + } + + } + + /** + * Decrypts XML file + * + * @param file path of the file to be decrypted + * @throws IOException if file could not be found + */ + public static void decrypt(File file) throws IOException { + + try { + Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); + SecretKey secretKey = generateKey(); + cipher.init(Cipher.DECRYPT_MODE, secretKey); + fileToBytes(cipher, file); + } catch (GeneralSecurityException gse) { + logger.severe("Cipher or Padding might not be supported " + gse.getMessage()); + } catch (UnsupportedEncodingException use) { + logger.info("Encoding Unsupported " + use.getMessage()); + } + } + + /** + * Processes the given file using the given cipher + * + * @param cipher cipher used for encryption or decryption + * @param file path of the file to be encrypted or decrypted + * @throws IOException if file could not be found + */ + + private static void fileToBytes(Cipher cipher, File file) throws IOException { + + FileInputStream fileInputStream = null; + FileOutputStream fileOutputStream = null; + try { + fileInputStream = new FileInputStream(file); + byte[] readBytes = new byte[(int) file.length()]; + fileInputStream.read(readBytes); + + byte[] writeBytes = cipher.doFinal(readBytes); + fileOutputStream = new FileOutputStream(file); + fileOutputStream.write(writeBytes); + + } catch (BadPaddingException be) { + logger.info("File might not decoded/encoded properly due to bad padding " + be.getMessage()); + } catch (IllegalBlockSizeException ibe) { + logger.info("Input length size must be in multiple of 16 " + ibe.getMessage()); + } finally { + try { + if (fileInputStream != null) { + fileInputStream.close(); + } + if (fileOutputStream != null) { + fileOutputStream.close(); + } + } catch (IOException ioe) { + logger.info("File streams could not be closed " + ioe.getMessage()); + } + } + } + + /** + * Method to generate a SecretKey using the password provided + * + * @return SecretKey generated using AES encryption + */ + public static SecretKey generateKey() { + + SecretKeySpec secretKeySpec = null; + try { + MessageDigest digester = MessageDigest.getInstance("SHA-256"); + digester.update(password.getBytes("UTF-8")); + byte[] key = digester.digest(); + secretKeySpec = new SecretKeySpec(key, 0, 16, "AES"); + } catch (NoSuchAlgorithmException nae) { + logger.info("Algorithm Unsupported " + nae.getMessage()); + } catch (UnsupportedEncodingException use) { + logger.info("Encoding Unsupported " + use.getMessage()); + } + + return secretKeySpec; + } +} diff --git a/src/main/java/seedu/budgeteer/commons/util/ExcelUtil.java b/src/main/java/seedu/budgeteer/commons/util/ExcelUtil.java new file mode 100755 index 000000000000..ea3c42a398b2 --- /dev/null +++ b/src/main/java/seedu/budgeteer/commons/util/ExcelUtil.java @@ -0,0 +1,337 @@ +//@@author ngkaicong +package seedu.budgeteer.commons.util; + +import static seedu.budgeteer.logic.parser.CliSyntax.PREFIX_TAG; + +import java.io.FileNotFoundException; +import java.io.FileOutputStream; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.logging.Logger; + +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellType; +import org.apache.poi.ss.usermodel.Chart; +import org.apache.poi.ss.usermodel.ClientAnchor; +import org.apache.poi.ss.usermodel.Drawing; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.charts.AxisCrosses; +import org.apache.poi.ss.usermodel.charts.AxisPosition; +import org.apache.poi.ss.usermodel.charts.ChartAxis; +import org.apache.poi.ss.usermodel.charts.ChartDataSource; +import org.apache.poi.ss.usermodel.charts.ChartLegend; +import org.apache.poi.ss.usermodel.charts.DataSources; +import org.apache.poi.ss.usermodel.charts.LegendPosition; +import org.apache.poi.ss.usermodel.charts.LineChartData; +import org.apache.poi.ss.usermodel.charts.LineChartSeries; +import org.apache.poi.ss.usermodel.charts.ValueAxis; +import org.apache.poi.ss.util.CellRangeAddress; +import org.apache.poi.xssf.usermodel.XSSFSheet; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; + +import seedu.budgeteer.commons.core.LogsCenter; +import seedu.budgeteer.commons.core.Messages; +import seedu.budgeteer.logic.parser.ArgumentMultimap; +import seedu.budgeteer.logic.parser.ArgumentTokenizer; +import seedu.budgeteer.logic.parser.ParserUtil; +import seedu.budgeteer.logic.parser.exceptions.ParseException; +import seedu.budgeteer.model.DirectoryPath; +import seedu.budgeteer.model.entry.CashFlow; +import seedu.budgeteer.model.entry.Date; +import seedu.budgeteer.model.entry.Entry; +import seedu.budgeteer.model.entry.Name; +import seedu.budgeteer.model.tag.Tag; +import seedu.budgeteer.ui.SummaryEntry; + +/** + * Transfer data into Excel file utilities. + */ +public class ExcelUtil { + private static final int FIRST_COLUMN = 0; + private static final int SECOND_COLUMN = 1; + private static final int THIRD_COLUMN = 2; + private static final int FOURTH_COLUMN = 3; + private static final int FIRST_ROW = 0; + private static final int SECOND_ROW = 1; + private static final int THIRD_ROW = 2; + private static final int FOURTH_ROW = 3; + private static final int MAXIMUM_GAP_BETWEEN_COLUMN = 4; + private static final int LEFT_OUT_CHARACTER = 4; + private static final int STARTING_INDEX = 0; + private static final int STARTING_SHEET = 0; + private static final int RECORD_EMPTY = 0; + private static final int STARTING_CURRENCY = 2; + private static final char MINUS_SIGN_CHAR = '-'; + private static final char PLUS_SIGN_CHAR = '+'; + private static final char CURRENCY_CHAR = '$'; + private static final Double CHANGE_TO_DOUBLE = 1.0; + private static final String PLUS_SIGN_STRING = "+"; + private static final String MINUS_SIGN_STRING = "-"; + private static final String CURRENCY_STRING = "$"; + private static final String WHITE_SPACE = " "; + private static final String NAME_TITLE = "NAME"; + private static final String DATE_TITLE = "DATE"; + private static final String MONEY_TITLE = "MONEY"; + private static final String TAG_TITLE = "TAGS"; + private static final String INCOME_TITLE = "INCOME"; + private static final String EXPENSE_TITLE = "EXPENSE"; + private static final String TOTAL_MONEY = "NET"; + private static final String TAG_SEPARATOR = " ... "; + + private static Logger logger = LogsCenter.getLogger(ExcelUtil.class); + + //==========================================MAIN METHOD============================================================ + + /** + * Write the excel sheet into Directory. + */ + public static void writeExcelSheetIntoDirectory (List entryList, + List daySummaryEntryList, + XSSFSheet recordDataSheet, XSSFSheet summaryDataSheet, + XSSFWorkbook workbook, String filePath) { + try { + writeDataIntoExcelSheetRecord(entryList, recordDataSheet); + writeDataIntoExcelSheetSummary(daySummaryEntryList, summaryDataSheet); + //Write the workbook in file system + FileOutputStream out = new FileOutputStream(filePath, false); + workbook.write(out); + out.close(); + drawChart(summaryDataSheet, filePath, workbook); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * Draw the line chart. + */ + public static void drawChart (XSSFSheet sheet, String filePath, XSSFWorkbook workbook) + throws FileNotFoundException { + try { + final int firstRowSheet = sheet.getFirstRowNum() + SECOND_ROW; + final int lastRowSheet = sheet.getLastRowNum(); + final int firstColumnSheet = sheet.getRow(firstRowSheet).getFirstCellNum(); + final int lastColumnSheet = firstColumnSheet; + final int firstColumnIncome = firstColumnSheet + SECOND_COLUMN; + final int lastColumnIncome = firstColumnIncome; + final int firstColumnExpense = firstColumnSheet + THIRD_COLUMN; + final int lastColumnExpense = firstColumnExpense; + final int firstColumnNet = firstColumnSheet + FOURTH_COLUMN; + final int lastColumnNet = firstColumnNet; + final int widthChart = 20; + final int heightChart = 30; + + if (!DirectoryPath.isValidFilePath(filePath)) { + throw new ParseException(Messages.MESSAGE_UNREALISTIC_DIRECTORY); + } + + Drawing drawing = sheet.createDrawingPatriarch(); + ClientAnchor anchor = drawing.createAnchor(0, 0, 0, 0, + firstColumnSheet + FOURTH_COLUMN + THIRD_COLUMN, firstRowSheet, + lastColumnSheet + widthChart, firstRowSheet + heightChart); + + Chart chart = drawing.createChart(anchor); + ChartLegend legend = chart.getOrCreateLegend(); + legend.setPosition(LegendPosition.TOP_RIGHT); + + LineChartData data = chart.getChartDataFactory().createLineChartData(); + + // Use a category axis for the bottom axis. + ChartAxis bottomAxis = chart.getChartAxisFactory().createCategoryAxis(AxisPosition.BOTTOM); + ValueAxis leftAxis = chart.getChartAxisFactory().createValueAxis(AxisPosition.LEFT); + leftAxis.setCrosses(AxisCrosses.AUTO_ZERO); + + ChartDataSource xDate = DataSources.fromStringCellRange( + sheet, new CellRangeAddress(firstRowSheet, lastRowSheet, firstColumnSheet, lastColumnSheet)); + ChartDataSource yIncome = DataSources.fromNumericCellRange( + sheet, new CellRangeAddress(firstRowSheet, lastRowSheet, firstColumnIncome, lastColumnIncome)); + ChartDataSource yExpense = DataSources.fromNumericCellRange( + sheet, new CellRangeAddress(firstRowSheet, lastRowSheet, firstColumnExpense, lastColumnExpense)); + ChartDataSource yNet = DataSources.fromNumericCellRange( + sheet, new CellRangeAddress(firstRowSheet, lastRowSheet, firstColumnNet, lastColumnNet)); + + LineChartSeries series1 = data.addSeries(xDate, yIncome); + series1.setTitle("Income"); + LineChartSeries series2 = data.addSeries(xDate, yExpense); + series2.setTitle("Expense"); + LineChartSeries series3 = data.addSeries(xDate, yNet); + series3.setTitle("Net"); + + chart.plot(data, bottomAxis, leftAxis); + + FileOutputStream fileOut = new FileOutputStream(filePath); + workbook.write(fileOut); + fileOut.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * Change the String of Money into appropriate format, as positive number won't have + sign, so we have to add it. + * For positive number, the "+" will be discarded when you try to add money into Financial Planner --> error. + */ + public static String checkMoneyString (String moneyString) { + return (moneyString.charAt(STARTING_INDEX) == MINUS_SIGN_CHAR) + || moneyString.charAt(STARTING_INDEX) == PLUS_SIGN_CHAR + ? (moneyString) : (PLUS_SIGN_STRING + moneyString); + } + + /** + * Return string for each specific cell, as different method for different column of 1 row. + */ + public static String retrieveDataFromOneRow (Row row, Cell cell, int columnIndex) { + if (isStringCellType(cell) && cell != null) { + return cell.getStringCellValue().trim(); + } + if (isNumericCellType(cell) && cell != null && columnIndex == THIRD_COLUMN) { + return Double.toString(cell.getNumericCellValue() * CHANGE_TO_DOUBLE); + } + return null; + } + + /** + * Create a records with data given. + */ + private static Entry createRecord(String nameString, String dateString, String moneyString, String tagsString) + throws ParseException { + Name nameParse = ParserUtil.parseName(nameString); + Date dateParse = ParserUtil.parseDate(dateString); + CashFlow cashFlow = ParserUtil.parseCashFlow(moneyString); + Set tagList = new HashSet<>(); + if (tagsString != null) { + String processedTags = tagsString.replace(TAG_SEPARATOR, WHITE_SPACE + PREFIX_TAG); + System.out.println("TAG ADDED: " + PREFIX_TAG + processedTags); + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize( + WHITE_SPACE + PREFIX_TAG + processedTags, PREFIX_TAG); + tagList = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG)); + } + return new Entry(nameParse, dateParse, cashFlow, tagList); + } + + /** + * Check if the Cell type is String. + */ + private static Boolean isStringCellType(Cell cell) { + return (cell.getCellTypeEnum() == CellType.STRING); + } + + /** + * Check if the Cell type is Numeric. + */ + private static Boolean isNumericCellType(Cell cell) { + return (cell.getCellTypeEnum() == CellType.NUMERIC); + } + + //==========================================SUB METHOD FOR EXPORT EXCEL============================================= + + /** + * Write Entry data into Excel Sheet. + */ + private static void writeDataIntoExcelSheetRecord (List entries, XSSFSheet sheet) { + logger.info("----------------------------------------------------------START WRITE INTO EXCEL FILE"); + int rowNum = STARTING_INDEX; + Row startingRow = sheet.createRow(rowNum); + writeDataIntoCell(startingRow, FIRST_COLUMN, NAME_TITLE); + writeDataIntoCell(startingRow, SECOND_COLUMN, DATE_TITLE); + writeDataIntoCell(startingRow, THIRD_COLUMN, MONEY_TITLE); + writeDataIntoCell(startingRow, FOURTH_COLUMN, TAG_TITLE); + + for (Entry entry : entries) { + Row row = sheet.createRow(++rowNum); + StringBuilder stringBuilder = new StringBuilder(); + writeDataIntoCell(row, FIRST_COLUMN, entry.getName().fullName); + writeDataIntoCell(row, SECOND_COLUMN, entry.getDate().value); + writeDataIntoCell(row, THIRD_COLUMN, entry.getCashFlow().valueDouble); + if (entry.getTags().size() > RECORD_EMPTY) { + for (Tag tag : entry.getTags()) { + stringBuilder.append(tag.tagName + TAG_SEPARATOR); + } + writeDataIntoCell(row, FOURTH_COLUMN, stringBuilder.toString() + .substring(STARTING_INDEX, stringBuilder.toString().length() - LEFT_OUT_CHARACTER)); + logger.info("---------------------Tag: " + stringBuilder.toString() + .substring(STARTING_INDEX, stringBuilder.toString().length() - LEFT_OUT_CHARACTER)); + } + } + } + + /** + * Write Summary data into Excel Sheet. + */ + private static void writeDataIntoExcelSheetSummary (List daySummaryEntryList, XSSFSheet sheet) { + int rowNum = STARTING_INDEX; + Row startingRow = sheet.createRow(rowNum); + writeDataIntoCell(startingRow, FIRST_COLUMN, DATE_TITLE); + writeDataIntoCell(startingRow, SECOND_COLUMN, INCOME_TITLE); + writeDataIntoCell(startingRow, THIRD_COLUMN, EXPENSE_TITLE); + writeDataIntoCell(startingRow, FOURTH_COLUMN, TOTAL_MONEY); + for (SummaryEntry summaryEntry : daySummaryEntryList) { + Row row = sheet.createRow(++rowNum); + writeDataIntoCell(row, FIRST_COLUMN, + summaryEntry.getIdentifier()); + writeDataIntoCell(row, SECOND_COLUMN, + Double.parseDouble(removeCurrencySign(summaryEntry.getTotalIncome()))); + writeDataIntoCell(row, THIRD_COLUMN, + Double.parseDouble(removeCurrencySign((summaryEntry.getTotalExpense())))); + writeDataIntoCell(row, FOURTH_COLUMN, + Double.parseDouble(removeCurrencySign(summaryEntry.getTotal()))); + } + } + + /** + * Remove the character of $ in the String money retrieved. + */ + private static String removeCurrencySign (String money) { + String moneyString = null; + if (money.contains(CURRENCY_STRING)) { + for (int i = STARTING_INDEX; i < money.length(); i++) { + if (money.charAt(i) == CURRENCY_CHAR) { + moneyString = (money.charAt(STARTING_INDEX) == MINUS_SIGN_CHAR) + ? (MINUS_SIGN_STRING + money.substring(++i)) + : (PLUS_SIGN_STRING + money.substring(++i)); + } + } + } else { + moneyString = (money.charAt(STARTING_INDEX) == MINUS_SIGN_CHAR) + ? (PLUS_SIGN_STRING + money) + : money; + } + return moneyString; + } + + /** + * Write data into cell. + */ + private static void writeDataIntoCell (Row row, int colNum, Object object) { + if (object instanceof String) { + row.createCell(colNum).setCellValue((String) object); + } else { + row.createCell(colNum).setCellValue((Double) object); + } + } + + /** + * Create the fileName path. + */ + public static String setPathFile (String nameFile, String directoryPath) { + String checkedNameFile; + + checkedNameFile = (nameFile.length() > 5 + && nameFile.substring(nameFile.length() - 5, nameFile.length()).equals(".xlsx")) + ? nameFile : (nameFile + ".xlsx"); + logger.info("=----------------------------------------" + checkedNameFile); + return directoryPath + (System.getProperty("file.separator") + checkedNameFile); + } + /** + * Set the name for the Excel file based on type of inputs. + */ + public static String setNameExcelFile (Date startDate, Date endDate) { + return (startDate == null && endDate == null) + ? "ENTRIES_ALL.xlsx" + : ((startDate.equals(endDate)) + ? String.format("ENTRIES_%1$s.xlsx", startDate.getValue()) + : String.format("ENTRIES_%1$s_%2$s.xlsx", startDate.getValue(), endDate.getValue())); + } +} diff --git a/src/main/java/seedu/budgeteer/commons/util/FileUtil.java b/src/main/java/seedu/budgeteer/commons/util/FileUtil.java new file mode 100644 index 000000000000..6dcc302fde0c --- /dev/null +++ b/src/main/java/seedu/budgeteer/commons/util/FileUtil.java @@ -0,0 +1,168 @@ +package seedu.budgeteer.commons.util; +import static seedu.budgeteer.commons.util.AppUtil.checkArgument; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * Writes and reads files + */ +public class FileUtil { + + private static final String CHARSET = "UTF-8"; + + public static boolean isPassExists(File file) { + return file.exists(); + } + + public static boolean isFileExists(File file) { + return file.exists() && file.isFile(); + } + + public static boolean isFileExists(Path file) { + return Files.exists(file) && Files.isRegularFile(file); + } + + /** + * Returns true if {@code path} can be converted into a {@code Path} via {@link Paths#get(String)}, + * otherwise returns false. + * @param path A string representing the file path. Cannot be null. + */ + public static boolean isValidPath(String path) { + try { + Paths.get(path); + } catch (InvalidPathException ipe) { + return false; + } + return true; + } + + /** + * Creates a file if it does not exist along with its missing parent directories. + * @throws IOException if the file or directory cannot be created. + */ + public static void createIfMissing(Path file) throws IOException { + if (!isFileExists(file)) { + createFile(file); + } + } + + /** + * Creates a file if it does not exist along with its missing parent directories. + * @throws IOException if the file or directory cannot be created. + */ + public static void createIfMissing(File file) throws IOException { + if (!isFileExists(file)) { + createFile(file); + } + } + + /** + * Creates a file if it does not exist along with its missing parent directories. + */ + public static void createFile(Path file) throws IOException { + if (Files.exists(file)) { + return; + } + + createParentDirsOfFile(file); + + Files.createFile(file); + } + + /** + * Creates a file if it does not exist along with its missing parent directories + * + * @return true if file is created, false if file already exists + */ + public static boolean createFile(File file) throws IOException { + if (file.exists()) { + return false; + } + + createParentDirsOfFile(file); + + return file.createNewFile(); + } + + /** + * Creates parent directories of file if it has a parent directory + */ + public static void createParentDirsOfFile(Path file) throws IOException { + Path parentDir = file.getParent(); + + if (parentDir != null) { + Files.createDirectories(parentDir); + } + } + + /** + * Creates parent directories of file if it has a parent directory + */ + public static void createParentDirsOfFile(File file) throws IOException { + File parentDir = file.getParentFile(); + + if (parentDir != null) { + createDirs(parentDir); + } + } + + /** + * Assumes file exists + */ + public static String readFromFile(Path file) throws IOException { + return new String(Files.readAllBytes(file), CHARSET); + } + + /** + * Assumes file exists + */ + public static String readFromFile(File file) throws IOException { + return new String(Files.readAllBytes(file.toPath()), CHARSET); + } + + /** + * Writes given string to a file. + * Will create the file if it does not exist yet. + */ + public static void writeToFile(Path file, String content) throws IOException { + Files.write(file, content.getBytes(CHARSET)); + } + + /** + * Writes given string to a file. + * Will create the file if it does not exist yet. + */ + public static void writeToFile(File file, String content) throws IOException { + Files.write(file.toPath(), content.getBytes(CHARSET)); + } + + + /** + * Creates the given directory along with its parent directories + * + * @param dir the directory to be created; assumed not null + * @throws IOException if the directory or a parent directory cannot be created + */ + public static void createDirs(File dir) throws IOException { + if (!dir.exists() && !dir.mkdirs()) { + throw new IOException("Failed to make directories of " + dir.getName()); + } + } + + + /** + * Converts a string to a platform-specific file path + * @param pathWithForwardSlash A String representing a file path but using '/' as the separator + * @return {@code pathWithForwardSlash} but '/' replaced with {@code File.separator} + */ + public static String getPath(String pathWithForwardSlash) { + checkArgument(pathWithForwardSlash.contains("/")); + return pathWithForwardSlash.replace("/", File.separator); + } + +} diff --git a/src/main/java/seedu/address/commons/util/InvalidationListenerManager.java b/src/main/java/seedu/budgeteer/commons/util/InvalidationListenerManager.java similarity index 97% rename from src/main/java/seedu/address/commons/util/InvalidationListenerManager.java rename to src/main/java/seedu/budgeteer/commons/util/InvalidationListenerManager.java index 70165336db6d..09e9044efd15 100644 --- a/src/main/java/seedu/address/commons/util/InvalidationListenerManager.java +++ b/src/main/java/seedu/budgeteer/commons/util/InvalidationListenerManager.java @@ -1,4 +1,4 @@ -package seedu.address.commons.util; +package seedu.budgeteer.commons.util; import static java.util.Objects.requireNonNull; diff --git a/src/main/java/seedu/address/commons/util/JsonUtil.java b/src/main/java/seedu/budgeteer/commons/util/JsonUtil.java similarity index 97% rename from src/main/java/seedu/address/commons/util/JsonUtil.java rename to src/main/java/seedu/budgeteer/commons/util/JsonUtil.java index 8ef609f055df..bf320aff0fa3 100644 --- a/src/main/java/seedu/address/commons/util/JsonUtil.java +++ b/src/main/java/seedu/budgeteer/commons/util/JsonUtil.java @@ -1,4 +1,4 @@ -package seedu.address.commons.util; +package seedu.budgeteer.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 seedu.budgeteer.commons.core.LogsCenter; +import seedu.budgeteer.commons.exceptions.DataConversionException; /** * Converts a Java object instance to JSON and vice versa diff --git a/src/main/java/seedu/budgeteer/commons/util/MoneyUtil.java b/src/main/java/seedu/budgeteer/commons/util/MoneyUtil.java new file mode 100755 index 000000000000..a107e888f29f --- /dev/null +++ b/src/main/java/seedu/budgeteer/commons/util/MoneyUtil.java @@ -0,0 +1,37 @@ +package seedu.budgeteer.commons.util; + +import static seedu.budgeteer.commons.util.CollectionUtil.requireAllNonNull; +import static seedu.budgeteer.model.entry.CashFlow.FORMAT_STANDARD_MONEY; +import static seedu.budgeteer.model.entry.CashFlow.REPRESENTATION_ZERO; + +import seedu.budgeteer.model.entry.CashFlow; + +/** + * Contains helper methods to process data of {@code CashFlow} + */ +public class MoneyUtil { + + /** + * Takes in 2 {@code CashFlow} parameters and returns the sum + */ + public static CashFlow add(CashFlow money1, CashFlow money2) { + requireAllNonNull(money1, money2); + Double newMoney = money1.toDouble() + money2.toDouble(); + return CashFlow.getCashFlow(formatIntoMoneyFlowFormat(newMoney)); + } + + /** + * Formats a string into a string that is readable by {@code CashFlow} + */ + private static String formatIntoMoneyFlowFormat(Double money) { + String formattedMoney; + if (money == 0) { + formattedMoney = REPRESENTATION_ZERO; + } else if (money > 0) { + formattedMoney = String.format("+" + FORMAT_STANDARD_MONEY, money); + } else { + formattedMoney = String.format(FORMAT_STANDARD_MONEY, money); + } + return formattedMoney; + } +} diff --git a/src/main/java/seedu/address/commons/util/StringUtil.java b/src/main/java/seedu/budgeteer/commons/util/StringUtil.java similarity index 95% rename from src/main/java/seedu/address/commons/util/StringUtil.java rename to src/main/java/seedu/budgeteer/commons/util/StringUtil.java index 61cc8c9a1cb8..c93e6c3bfb6b 100644 --- a/src/main/java/seedu/address/commons/util/StringUtil.java +++ b/src/main/java/seedu/budgeteer/commons/util/StringUtil.java @@ -1,7 +1,7 @@ -package seedu.address.commons.util; +package seedu.budgeteer.commons.util; import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; +import static seedu.budgeteer.commons.util.AppUtil.checkArgument; import java.io.PrintWriter; import java.io.StringWriter; diff --git a/src/main/java/seedu/address/logic/CommandHistory.java b/src/main/java/seedu/budgeteer/logic/CommandHistory.java similarity index 98% rename from src/main/java/seedu/address/logic/CommandHistory.java rename to src/main/java/seedu/budgeteer/logic/CommandHistory.java index 404675e43811..2c279090ab39 100644 --- a/src/main/java/seedu/address/logic/CommandHistory.java +++ b/src/main/java/seedu/budgeteer/logic/CommandHistory.java @@ -1,4 +1,4 @@ -package seedu.address.logic; +package seedu.budgeteer.logic; import static java.util.Objects.requireNonNull; diff --git a/src/main/java/seedu/address/logic/Logic.java b/src/main/java/seedu/budgeteer/logic/Logic.java similarity index 52% rename from src/main/java/seedu/address/logic/Logic.java rename to src/main/java/seedu/budgeteer/logic/Logic.java index 60369e2074e4..57caf6a94a6a 100644 --- a/src/main/java/seedu/address/logic/Logic.java +++ b/src/main/java/seedu/budgeteer/logic/Logic.java @@ -1,15 +1,15 @@ -package seedu.address.logic; +package seedu.budgeteer.logic; import java.nio.file.Path; import javafx.beans.property.ReadOnlyProperty; 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; +import seedu.budgeteer.commons.core.GuiSettings; +import seedu.budgeteer.logic.commands.CommandResult; +import seedu.budgeteer.logic.commands.exceptions.CommandException; +import seedu.budgeteer.logic.parser.exceptions.ParseException; +import seedu.budgeteer.model.ReadOnlyEntriesBook; +import seedu.budgeteer.model.entry.Entry; /** * API of the Logic component @@ -25,14 +25,14 @@ public interface Logic { CommandResult execute(String commandText) throws CommandException, ParseException; /** - * Returns the AddressBook. + * Returns the EntriesBook. * - * @see seedu.address.model.Model#getAddressBook() + * @see seedu.budgeteer.model.Model#getAddressBook() */ - ReadOnlyAddressBook getAddressBook(); + ReadOnlyEntriesBook getAddressBook(); - /** Returns an unmodifiable view of the filtered list of persons */ - ObservableList getFilteredPersonList(); + /** Returns an unmodifiable view of the filtered list of entrys */ + ObservableList getFilteredEntryList(); /** * Returns an unmodifiable view of the list of commands entered by the user. @@ -41,7 +41,7 @@ public interface Logic { ObservableList getHistory(); /** - * Returns the user prefs' address book file path. + * Returns the user prefs' budgeteer book file path. */ Path getAddressBookFilePath(); @@ -56,17 +56,19 @@ public interface Logic { void setGuiSettings(GuiSettings guiSettings); /** - * Selected person in the filtered person list. - * null if no person is selected. + * Selected entry in the filtered entry list. + * null if no entry is selected. * - * @see seedu.address.model.Model#selectedPersonProperty() + * @see seedu.budgeteer.model.Model#selectedEntryProperty() */ - ReadOnlyProperty selectedPersonProperty(); + ReadOnlyProperty selectedEntryProperty(); /** - * Sets the selected person in the filtered person list. + * Sets the selected entry in the filtered entry list. * - * @see seedu.address.model.Model#setSelectedPerson(Person) + * @see seedu.budgeteer.model.Model#setSelectedEntry(Entry) */ - void setSelectedPerson(Person person); + void setSelectedEntry(Entry entry); + + } diff --git a/src/main/java/seedu/budgeteer/logic/LogicManager.java b/src/main/java/seedu/budgeteer/logic/LogicManager.java new file mode 100644 index 000000000000..4cc9e15310bc --- /dev/null +++ b/src/main/java/seedu/budgeteer/logic/LogicManager.java @@ -0,0 +1,171 @@ +package seedu.budgeteer.logic; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.logging.Logger; + +import javafx.beans.property.ReadOnlyProperty; +import javafx.collections.ObservableList; +import seedu.budgeteer.commons.core.GuiSettings; +import seedu.budgeteer.commons.core.LogsCenter; +import seedu.budgeteer.commons.util.EncryptionUtil; +import seedu.budgeteer.logic.commands.Command; +import seedu.budgeteer.logic.commands.CommandResult; +import seedu.budgeteer.logic.commands.LockCommand; +import seedu.budgeteer.logic.commands.exceptions.CommandException; +import seedu.budgeteer.logic.parser.EntriesBookParser; +import seedu.budgeteer.logic.parser.exceptions.ParseException; +import seedu.budgeteer.model.Model; +import seedu.budgeteer.model.ReadOnlyEntriesBook; +import seedu.budgeteer.model.UserPrefs; +import seedu.budgeteer.model.entry.Entry; +import seedu.budgeteer.storage.PasswordManager; +import seedu.budgeteer.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 CommandHistory history; + private final EntriesBookParser entriesBookParser; + private boolean isLocked; + private boolean addressBookModified; + + public LogicManager(Model model, Storage storage) { + this.model = model; + this.storage = storage; + history = new CommandHistory(); + entriesBookParser = new EntriesBookParser(); + updateLockedStatus(); + + // Set addressBookModified to true whenever the models' budgeteer book is modified. + model.getAddressBook().addListener(observable -> addressBookModified = true); + + } + + /** + * Check and updates the locked status within the class. If locked, hide all entries + */ + private void updateLockedStatus() { + this.isLocked = PasswordManager.passwordExists(); + if (isLocked) { + model.updateFilteredEntryList(Model.PREDICATE_HIDE_ALL_ENTRYS); + } + + } + + @Override + public CommandResult execute(String commandText) throws CommandException, ParseException { + logger.info("----------------[USER COMMAND][" + commandText + "]"); + addressBookModified = false; + updateLockedStatus(); + CommandResult commandResult; + + if (isLocked) { + tryUnlock(commandText); + if (isLocked) { + throw new CommandException(LockCommand.MESSAGE_WRONG_PASSWORD); + } else { + decryptFile(); + model.updateFilteredEntryList(Model.PREDICATE_SHOW_ALL_ENTRYS); + new LockCommand(new LockCommand.ClearLock(commandText)).execute(model, history); + return new CommandResult("Welcome to Budgeter"); + } + } + + try { + Command command = entriesBookParser.parseCommand(commandText); + commandResult = command.execute(model, history); + } finally { + history.add(commandText); + updateLockedStatus(); + } + + if (addressBookModified) { + logger.info("Address book modified, saving to file."); + try { + storage.saveAddressBook(model.getAddressBook()); + } catch (IOException ioe) { + throw new CommandException(FILE_OPS_ERROR_MESSAGE + ioe, ioe); + } + } + + return commandResult; + } + + /** + * Method to decrypt file + */ + private void decryptFile() { + try { + UserPrefs userPrefs = new UserPrefs(); + File file = new File(String.valueOf(userPrefs.getAddressBookFilePath())); + EncryptionUtil.decrypt(file); + } catch (IOException ioe) { + logger.warning("File not found" + ioe.getMessage()); + } + } + + /** + * Checks with the PasswordManger on whether to unlock the program + * @param commandText password provided + * @throws CommandException if password file is corrupted + */ + private void tryUnlock(String commandText) throws CommandException { + + try { + isLocked = !PasswordManager.verifyPassword(commandText); + } catch (IOException ioe) { + throw new CommandException("Unable to open password file"); + } + } + + + + @Override + public ObservableList getHistory() { + return history.getHistory(); + } + + @Override + public ObservableList getFilteredEntryList() { + return model.getFilteredEntryList(); + } + + @Override + public Path getAddressBookFilePath() { + return model.getAddressBookFilePath(); + } + + @Override + public GuiSettings getGuiSettings() { + return model.getGuiSettings(); + } + + @Override + public void setGuiSettings(GuiSettings guiSettings) { + model.setGuiSettings(guiSettings); + } + + @Override + public ReadOnlyProperty selectedEntryProperty() { + return model.selectedEntryProperty(); + } + + @Override + public void setSelectedEntry(Entry entry) { + model.setSelectedEntry(entry); + } + + @Override + public ReadOnlyEntriesBook getAddressBook() { + return model.getAddressBook(); + } + +} diff --git a/src/main/java/seedu/budgeteer/logic/PasswordAccepted.java b/src/main/java/seedu/budgeteer/logic/PasswordAccepted.java new file mode 100644 index 000000000000..321ae4258f38 --- /dev/null +++ b/src/main/java/seedu/budgeteer/logic/PasswordAccepted.java @@ -0,0 +1,12 @@ +package seedu.budgeteer.logic; + +/** + * Indicates that password is accepted + */ +public class PasswordAccepted extends Start { + + @Override + public String toString() { + return this.getClass().getSimpleName(); + } +} diff --git a/src/main/java/seedu/budgeteer/logic/PasswordCenter.java b/src/main/java/seedu/budgeteer/logic/PasswordCenter.java new file mode 100644 index 000000000000..a0759d6d64c0 --- /dev/null +++ b/src/main/java/seedu/budgeteer/logic/PasswordCenter.java @@ -0,0 +1,45 @@ +package seedu.budgeteer.logic; + +import java.util.logging.Logger; + +import com.google.common.eventbus.EventBus; + +import seedu.budgeteer.commons.core.LogsCenter; + +/** + * Manages the password dispatching of the app. + */ +public class PasswordCenter { + private static final Logger logger = LogsCenter.getLogger(PasswordCenter.class); + private static PasswordCenter instance; + private final EventBus eventBus; + + private PasswordCenter() { + eventBus = new EventBus(); + } + + public static PasswordCenter getInstance() { + if (instance == null) { + instance = new PasswordCenter(); + } + return instance; + } + + public static void clearSubscribers() { + instance = null; + } + + public void registerHandler(Object handler) { + eventBus.register(handler); + } + + /** + * Posts an event to the event bus. + */ + public PasswordCenter post(E event) { + logger.info("------[Event Posted] " + event.getClass().getCanonicalName() + ": " + event.toString()); + eventBus.post(event); + return this; + } + +} diff --git a/src/main/java/seedu/budgeteer/logic/Start.java b/src/main/java/seedu/budgeteer/logic/Start.java new file mode 100644 index 000000000000..3ec435d9d84d --- /dev/null +++ b/src/main/java/seedu/budgeteer/logic/Start.java @@ -0,0 +1,16 @@ +package seedu.budgeteer.logic; + +/** + * The base class for all event classes. + */ +public abstract class Start { + + /** + * All Events should have a clear unambiguous custom toString message so that feedback message creation + * stays consistent and reusable. + * + * For example, the event manager post method will call any posted event's toString and print it in the console. + */ + public abstract String toString(); + +} diff --git a/src/main/java/seedu/budgeteer/logic/commands/AddCommand.java b/src/main/java/seedu/budgeteer/logic/commands/AddCommand.java new file mode 100644 index 000000000000..711124269269 --- /dev/null +++ b/src/main/java/seedu/budgeteer/logic/commands/AddCommand.java @@ -0,0 +1,65 @@ +package seedu.budgeteer.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.budgeteer.logic.parser.CliSyntax.PREFIX_CASHFLOW; +import static seedu.budgeteer.logic.parser.CliSyntax.PREFIX_DATE; +import static seedu.budgeteer.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.budgeteer.logic.parser.CliSyntax.PREFIX_TAG; + +import seedu.budgeteer.logic.CommandHistory; +import seedu.budgeteer.logic.commands.exceptions.CommandException; +import seedu.budgeteer.model.Model; +import seedu.budgeteer.model.entry.Entry; + +/** + * Adds a entry to the budgeteer book. + */ +public class AddCommand extends Command { + + public static final String COMMAND_WORD = "add"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a entry to the budgeteer book. " + + "Parameters: " + + PREFIX_NAME + "NAME " + + PREFIX_DATE + "DATE " + + PREFIX_CASHFLOW + "CASHFLOW " + + "[" + PREFIX_TAG + "TAG]...\n" + + "Example: " + COMMAND_WORD + " " + + PREFIX_NAME + "Salary from John Doe " + + PREFIX_DATE + "01-01-2019 " + + PREFIX_CASHFLOW + "+100 " + + PREFIX_TAG + "friends "; + + public static final String MESSAGE_SUCCESS = "New entry added: %1$s"; + public static final String MESSAGE_DUPLICATE_ENTRY = "This entry already exists in the budgeteer book"; + + private final Entry toAdd; + + /** + * Creates an AddCommand to add the specified {@code Entry} + */ + public AddCommand(Entry entry) { + requireNonNull(entry); + toAdd = entry; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + requireNonNull(model); + + // if (model.hasEntry(toAdd)) { + // throw new CommandException(MESSAGE_DUPLICATE_ENTRY); + // } + + model.addEntry(toAdd); + model.commitAddressBook(); + 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/budgeteer/logic/commands/BitcoinCommand.java b/src/main/java/seedu/budgeteer/logic/commands/BitcoinCommand.java new file mode 100644 index 000000000000..9f94c730bf4c --- /dev/null +++ b/src/main/java/seedu/budgeteer/logic/commands/BitcoinCommand.java @@ -0,0 +1,47 @@ +package seedu.budgeteer.logic.commands; + +import static seedu.budgeteer.model.Model.PREDICATE_SHOW_ALL_ENTRYS; + +import javafx.collections.ObservableList; +import seedu.budgeteer.commons.util.CryptoUtil; +import seedu.budgeteer.logic.CommandHistory; +import seedu.budgeteer.model.Model; +import seedu.budgeteer.model.entry.Entry; +import seedu.budgeteer.model.entry.ReportEntryList; + + +/** + * Returns how many Bitcoin you can buy at the current market price. + */ +public class BitcoinCommand extends Command { + + public static final String COMMAND_WORD = "bitcoin"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Displays how much bitcoin you can buy.\n" + + "Example: " + COMMAND_WORD; + + public static final String MESSAGE_SUCCESS_HEADER = "You are able to buy "; + + + @Override + public CommandResult execute(Model model, CommandHistory history) { + CryptoUtil cryptoUtil = CryptoUtil.getInstance(); + double price = cryptoUtil.getBtc(); + + model.updateFilteredEntryList(PREDICATE_SHOW_ALL_ENTRYS); + ObservableList filteredList = model.getFilteredEntryList(); + ReportEntryList reportList = new ReportEntryList(filteredList); + Double total = reportList.getTotal(); + + Double amount = total / price; + amount = (double) Math.round(amount * 100.0) / 100.0; + + String successMessage = MESSAGE_SUCCESS_HEADER + amount.toString() + " BTC."; + + int roundOff = (int) Math.round(price); + + String currentPrice = " The current price of bitcoin is $" + roundOff + "."; + successMessage = successMessage + currentPrice; + return new CommandResult(successMessage); + } +} diff --git a/src/main/java/seedu/address/logic/commands/ClearCommand.java b/src/main/java/seedu/budgeteer/logic/commands/ClearCommand.java similarity index 53% rename from src/main/java/seedu/address/logic/commands/ClearCommand.java rename to src/main/java/seedu/budgeteer/logic/commands/ClearCommand.java index a22219ad76ad..4e1bce68b2a5 100644 --- a/src/main/java/seedu/address/logic/commands/ClearCommand.java +++ b/src/main/java/seedu/budgeteer/logic/commands/ClearCommand.java @@ -1,24 +1,24 @@ -package seedu.address.logic.commands; +package seedu.budgeteer.logic.commands; import static java.util.Objects.requireNonNull; -import seedu.address.logic.CommandHistory; -import seedu.address.model.AddressBook; -import seedu.address.model.Model; +import seedu.budgeteer.logic.CommandHistory; +import seedu.budgeteer.model.EntriesBook; +import seedu.budgeteer.model.Model; /** - * Clears the address book. + * Clears the budgeteer book. */ public class ClearCommand extends Command { public static final String COMMAND_WORD = "clear"; - public static final String MESSAGE_SUCCESS = "Address book has been cleared!"; + public static final String MESSAGE_SUCCESS = "Bugeter has been cleared!"; @Override public CommandResult execute(Model model, CommandHistory history) { requireNonNull(model); - model.setAddressBook(new AddressBook()); + model.setAddressBook(new EntriesBook()); model.commitAddressBook(); return new CommandResult(MESSAGE_SUCCESS); } diff --git a/src/main/java/seedu/address/logic/commands/Command.java b/src/main/java/seedu/budgeteer/logic/commands/Command.java similarity index 76% rename from src/main/java/seedu/address/logic/commands/Command.java rename to src/main/java/seedu/budgeteer/logic/commands/Command.java index 34e99d786ec6..748ca3b96d4e 100644 --- a/src/main/java/seedu/address/logic/commands/Command.java +++ b/src/main/java/seedu/budgeteer/logic/commands/Command.java @@ -1,8 +1,8 @@ -package seedu.address.logic.commands; +package seedu.budgeteer.logic.commands; -import seedu.address.logic.CommandHistory; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.model.Model; +import seedu.budgeteer.logic.CommandHistory; +import seedu.budgeteer.logic.commands.exceptions.CommandException; +import seedu.budgeteer.model.Model; /** * Represents a command with hidden internal logic and the ability to be executed. diff --git a/src/main/java/seedu/address/logic/commands/CommandResult.java b/src/main/java/seedu/budgeteer/logic/commands/CommandResult.java similarity index 83% rename from src/main/java/seedu/address/logic/commands/CommandResult.java rename to src/main/java/seedu/budgeteer/logic/commands/CommandResult.java index 92f900b7916d..5f9cf8fc35a3 100644 --- a/src/main/java/seedu/address/logic/commands/CommandResult.java +++ b/src/main/java/seedu/budgeteer/logic/commands/CommandResult.java @@ -1,4 +1,4 @@ -package seedu.address.logic.commands; +package seedu.budgeteer.logic.commands; import static java.util.Objects.requireNonNull; @@ -14,15 +14,19 @@ public class CommandResult { /** Help information should be shown to the user. */ private final boolean showHelp; + /** Report should be shown to the user. */ + private final boolean showReport; + /** The application should exit. */ private final boolean exit; /** * Constructs a {@code CommandResult} with the specified fields. */ - public CommandResult(String feedbackToUser, boolean showHelp, boolean exit) { + public CommandResult(String feedbackToUser, boolean showHelp, boolean showReport, boolean exit) { this.feedbackToUser = requireNonNull(feedbackToUser); this.showHelp = showHelp; + this.showReport = showReport; this.exit = exit; } @@ -31,7 +35,7 @@ public CommandResult(String feedbackToUser, boolean showHelp, boolean exit) { * and other fields set to their default value. */ public CommandResult(String feedbackToUser) { - this(feedbackToUser, false, false); + this(feedbackToUser, false, false, false); } public String getFeedbackToUser() { @@ -42,6 +46,10 @@ public boolean isShowHelp() { return showHelp; } + public boolean isShowReport() { + return showReport; + } + public boolean isExit() { return exit; } diff --git a/src/main/java/seedu/budgeteer/logic/commands/CryptoCommand.java b/src/main/java/seedu/budgeteer/logic/commands/CryptoCommand.java new file mode 100644 index 000000000000..b0ea98485fe7 --- /dev/null +++ b/src/main/java/seedu/budgeteer/logic/commands/CryptoCommand.java @@ -0,0 +1,109 @@ +package seedu.budgeteer.logic.commands; + +import static seedu.budgeteer.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.budgeteer.model.Model.PREDICATE_SHOW_ALL_ENTRYS; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; + +import javafx.collections.ObservableList; +import seedu.budgeteer.logic.CommandHistory; +import seedu.budgeteer.model.Model; +import seedu.budgeteer.model.entry.Entry; +import seedu.budgeteer.model.entry.Name; +import seedu.budgeteer.model.entry.ReportEntryList; + +/** + * Returns how much cryptocurrency you can buy at the current market price. + */ +public class CryptoCommand extends Command { + + public static final String COMMAND_WORD = "crypto"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Displays how much of a certain " + + "cryptocurrency you can buy.\n" + + "Parameters: " + + PREFIX_NAME + "NAME " + + "Example: " + COMMAND_WORD + " " + + PREFIX_NAME + "CRYPTOCURRENCY NAME " + + "Example: " + COMMAND_WORD + " " + + PREFIX_NAME + "MSFT"; + + private static final String MESSAGE_SUCCESS = "The price of the cryptocurrency "; + + private String firstUrl = "https://min-api.cryptocompare.com/data/pricemulti?fsyms="; + private String secondUrl = "&tsyms=SGD"; + + private final Name name; + + public CryptoCommand(Name name) { + this.name = name; + } + + /** + * Function that calls the crypto API and returns the JSON in string format + */ + public String cryptoPrice() { + String ret = ""; + try { + String temp = firstUrl + name.fullName.toUpperCase() + secondUrl; + + URL url = new URL(temp); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("GET"); + conn.setRequestProperty("Accept", "application/json"); + + if (conn.getResponseCode() != 200) { + throw new RuntimeException("Failed : HTTP error code : " + + conn.getResponseCode()); + } + + BufferedReader br = new BufferedReader(new InputStreamReader((conn.getInputStream()))); + ret = br.readLine(); + + conn.disconnect(); + + } catch (MalformedURLException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + return ret; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) { + double price; + String messageReturn; + + model.updateFilteredEntryList(PREDICATE_SHOW_ALL_ENTRYS); + ObservableList filteredList = model.getFilteredEntryList(); + ReportEntryList reportList = new ReportEntryList(filteredList); + Double total = reportList.getTotal(); + + String full = cryptoPrice(); + + if (full == null || full.length() > 40) { + messageReturn = "Sorry, your input is not a valid cryptocurrency. Please try again."; + } else { + full = full.substring(14); + full = full.substring(0, full.length() - 2); + price = Float.parseFloat(full); + Double printPrice = (double) Math.round(price * 100.0) / 100.0; + + String first = "You are able to buy "; + Double amount = total / price; + amount = (double) Math.round(amount * 100.0) / 100.0; + String second = first + amount + " " + name.fullName.toUpperCase() + ". "; + + messageReturn = second + "The price of the cryptocurrency " + name.fullName.toUpperCase() + + " is $" + printPrice.toString() + "."; + } + + return new CommandResult(messageReturn); + } +} diff --git a/src/main/java/seedu/address/logic/commands/DeleteCommand.java b/src/main/java/seedu/budgeteer/logic/commands/DeleteCommand.java similarity index 55% rename from src/main/java/seedu/address/logic/commands/DeleteCommand.java rename to src/main/java/seedu/budgeteer/logic/commands/DeleteCommand.java index a20e9d49eac7..f72012f1305a 100644 --- a/src/main/java/seedu/address/logic/commands/DeleteCommand.java +++ b/src/main/java/seedu/budgeteer/logic/commands/DeleteCommand.java @@ -1,29 +1,29 @@ -package seedu.address.logic.commands; +package seedu.budgeteer.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.CommandHistory; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.model.Model; -import seedu.address.model.person.Person; +import seedu.budgeteer.commons.core.Messages; +import seedu.budgeteer.commons.core.index.Index; +import seedu.budgeteer.logic.CommandHistory; +import seedu.budgeteer.logic.commands.exceptions.CommandException; +import seedu.budgeteer.model.Model; +import seedu.budgeteer.model.entry.Entry; /** - * Deletes a person identified using it's displayed index from the address book. + * Deletes a entry identified using it's displayed index from the budgeteer 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" + + ": Deletes the entry identified by the index number used in the displayed entry 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"; + public static final String MESSAGE_DELETE_ENTRY_SUCCESS = "Deleted Entry: %1$s"; private final Index targetIndex; @@ -34,16 +34,16 @@ public DeleteCommand(Index targetIndex) { @Override public CommandResult execute(Model model, CommandHistory history) throws CommandException { requireNonNull(model); - List lastShownList = model.getFilteredPersonList(); + List lastShownList = model.getFilteredEntryList(); if (targetIndex.getZeroBased() >= lastShownList.size()) { - throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + throw new CommandException(Messages.MESSAGE_INVALID_ENTRY_DISPLAYED_INDEX); } - Person personToDelete = lastShownList.get(targetIndex.getZeroBased()); - model.deletePerson(personToDelete); + Entry entryToDelete = lastShownList.get(targetIndex.getZeroBased()); + model.deleteEntry(entryToDelete); model.commitAddressBook(); - return new CommandResult(String.format(MESSAGE_DELETE_PERSON_SUCCESS, personToDelete)); + return new CommandResult(String.format(MESSAGE_DELETE_ENTRY_SUCCESS, entryToDelete)); } @Override diff --git a/src/main/java/seedu/budgeteer/logic/commands/DisplayCommand.java b/src/main/java/seedu/budgeteer/logic/commands/DisplayCommand.java new file mode 100644 index 000000000000..e3f86dc653b6 --- /dev/null +++ b/src/main/java/seedu/budgeteer/logic/commands/DisplayCommand.java @@ -0,0 +1,69 @@ +package seedu.budgeteer.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import seedu.budgeteer.logic.CommandHistory; +import seedu.budgeteer.model.Model; + +/** + * Classifies all entrys in the current displayed list by a specified category and/or order. + * Keyword matching is case insensitive and regardless of order of entry. + */ +public class DisplayCommand extends Command { + + public static final String COMMAND_WORD = "display"; + + public static final int ONLY_CATEGORY_OR_ORDER_SPECIFIED = 1; + public static final int CATEGORY_AND_ORDER_SPECIFIED = 2; + public static final String DESCENDING_CONDITION = "des"; + public static final String ASCENDING_CONDITION = "asc"; + + public static final String MESSAGE_SUCCESS = "Entrys displayed by "; + + public static final String CATEGORY_NAME = "name"; + public static final String CATEGORY_CASHFLOW = "cashflow"; + public static final String CATEGORY_CASH = "cash"; + public static final String CATEGORY_DATE = "date"; + + public static final String ORDER_ASCENDING = "in ascending order.\n"; + public static final String ORDER_DESCENDING = "in descending order.\n"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Displays sorted entrys in the currently list " + + "by the specified category and order.\n" + + "Parameters: [CATEGORY] [ORDER]\n" + + "Example: " + COMMAND_WORD + " " + CATEGORY_NAME + " " + DESCENDING_CONDITION; + + public static final Set CATEGORY_SET = new HashSet<>(Arrays.asList(CATEGORY_NAME, CATEGORY_CASHFLOW, + CATEGORY_CASH, CATEGORY_DATE)); + public static final Set ORDER_SET = new HashSet<>(Arrays.asList(DESCENDING_CONDITION, ASCENDING_CONDITION)); + + private final String category; + private final Boolean ascending; + + public DisplayCommand(String category, Boolean ascending) { + this.category = category; + this.ascending = ascending; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) { + requireNonNull(model); + model.sortFilteredEntryList(category, ascending); + String returnMessageCategory = category; + String returnMessageOrder = ascending ? ORDER_ASCENDING : ORDER_DESCENDING; + return new CommandResult(MESSAGE_SUCCESS + returnMessageCategory + + returnMessageOrder); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof DisplayCommand // instanceof handles nulls + && category.equals(((DisplayCommand) other).category) + && ascending.equals(((DisplayCommand) other).ascending)); // state check + } +} diff --git a/src/main/java/seedu/budgeteer/logic/commands/EditCommand.java b/src/main/java/seedu/budgeteer/logic/commands/EditCommand.java new file mode 100644 index 000000000000..8ef46cad53f1 --- /dev/null +++ b/src/main/java/seedu/budgeteer/logic/commands/EditCommand.java @@ -0,0 +1,213 @@ +package seedu.budgeteer.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.budgeteer.logic.parser.CliSyntax.PREFIX_CASHFLOW; +import static seedu.budgeteer.logic.parser.CliSyntax.PREFIX_DATE; +import static seedu.budgeteer.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.budgeteer.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.budgeteer.model.Model.PREDICATE_SHOW_ALL_ENTRYS; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import seedu.budgeteer.commons.core.Messages; +import seedu.budgeteer.commons.core.index.Index; +import seedu.budgeteer.commons.util.CollectionUtil; +import seedu.budgeteer.logic.CommandHistory; +import seedu.budgeteer.logic.commands.exceptions.CommandException; +import seedu.budgeteer.model.Model; +import seedu.budgeteer.model.entry.CashFlow; +import seedu.budgeteer.model.entry.Date; +import seedu.budgeteer.model.entry.Entry; +import seedu.budgeteer.model.entry.Name; +import seedu.budgeteer.model.tag.Tag; + +/** + * Edits the details of an existing entry in the budgeteer 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 entry identified " + + "by the index number used in the displayed entry list. " + + "Existing values will be overwritten by the input values.\n" + + "Parameters: INDEX (must be a positive integer) " + + "[" + PREFIX_NAME + "NAME] " + + "[" + PREFIX_DATE + "DATE] " + + "[" + PREFIX_CASHFLOW + "CASHFLOW] " + + "[" + PREFIX_TAG + "TAG]...\n" + + "Example: " + COMMAND_WORD + " 1 " + + PREFIX_DATE + "01-01-2019 " + + PREFIX_CASHFLOW + "+100"; + + public static final String MESSAGE_EDIT_ENTRY_SUCCESS = "Edited Entry: %1$s"; + public static final String MESSAGE_NOT_EDITED = "At least one field to edit must be provided."; + public static final String MESSAGE_DUPLICATE_ENTRY = "This entry already exists in the budgeteer book."; + + private final Index index; + private final EditEntryDescriptor editEntryDescriptor; + + /** + * @param index of the entry in the filtered entry list to edit + * @param editEntryDescriptor details to edit the entry with + */ + public EditCommand(Index index, EditEntryDescriptor editEntryDescriptor) { + requireNonNull(index); + requireNonNull(editEntryDescriptor); + + this.index = index; + this.editEntryDescriptor = new EditEntryDescriptor(editEntryDescriptor); + } + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredEntryList(); + + if (index.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_ENTRY_DISPLAYED_INDEX); + } + + Entry entryToEdit = lastShownList.get(index.getZeroBased()); + Entry editedEntry = createEditedEntry(entryToEdit, editEntryDescriptor); + + if (!entryToEdit.isSameEntry(editedEntry) && model.hasEntry(editedEntry)) { + throw new CommandException(MESSAGE_DUPLICATE_ENTRY); + } + + model.setEntry(entryToEdit, editedEntry); + model.updateFilteredEntryList(PREDICATE_SHOW_ALL_ENTRYS); + model.commitAddressBook(); + return new CommandResult(String.format(MESSAGE_EDIT_ENTRY_SUCCESS, editedEntry)); + } + + /** + * Creates and returns a {@code Entry} with the details of {@code entryToEdit} + * edited with {@code editEntryDescriptor}. + */ + private static Entry createEditedEntry(Entry entryToEdit, EditEntryDescriptor editEntryDescriptor) { + assert entryToEdit != null; + + Name updatedName = editEntryDescriptor.getName().orElse(entryToEdit.getName()); + Date updatedDate = editEntryDescriptor.getDate().orElse(entryToEdit.getDate()); + CashFlow updatedCashFlow = editEntryDescriptor.getCashFlow().orElse(entryToEdit.getCashFlow()); + Set updatedTags = editEntryDescriptor.getTags().orElse(entryToEdit.getTags()); + + return new Entry(updatedName, updatedDate, updatedCashFlow, 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) + && editEntryDescriptor.equals(e.editEntryDescriptor); + } + + /** + * Stores the details to edit the entry with. Each non-empty field value will replace the + * corresponding field value of the entry. + */ + public static class EditEntryDescriptor { + private Name name; + private Date date; + private CashFlow cashFlow; + private Set tags; + + public EditEntryDescriptor() {} + + /** + * Copy constructor. + * A defensive copy of {@code tags} is used internally. + */ + public EditEntryDescriptor(EditEntryDescriptor toCopy) { + setName(toCopy.name); + setDate(toCopy.date); + setCashFlow(toCopy.cashFlow); + setTags(toCopy.tags); + } + + /** + * Returns true if at least one field is edited. + */ + public boolean isAnyFieldEdited() { + return CollectionUtil.isAnyNonNull(name, date, cashFlow, tags); + } + + public void setName(Name name) { + this.name = name; + } + + public Optional getName() { + return Optional.ofNullable(name); + } + + public void setDate(Date date) { + this.date = date; + } + + public Optional getDate() { + return Optional.ofNullable(date); + } + + public void setCashFlow(CashFlow cashFlow) { + this.cashFlow = cashFlow; + } + + public Optional getCashFlow() { + return Optional.ofNullable(cashFlow); + } + + /** + * 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 EditEntryDescriptor)) { + return false; + } + + // state check + EditEntryDescriptor e = (EditEntryDescriptor) other; + + return getName().equals(e.getName()) + && getDate().equals(e.getDate()) + && getCashFlow().equals(e.getCashFlow()) + && getTags().equals(e.getTags()); + } + } +} diff --git a/src/main/java/seedu/budgeteer/logic/commands/EthereumCommand.java b/src/main/java/seedu/budgeteer/logic/commands/EthereumCommand.java new file mode 100644 index 000000000000..6b373879d30c --- /dev/null +++ b/src/main/java/seedu/budgeteer/logic/commands/EthereumCommand.java @@ -0,0 +1,46 @@ +package seedu.budgeteer.logic.commands; + +import static seedu.budgeteer.model.Model.PREDICATE_SHOW_ALL_ENTRYS; + +import javafx.collections.ObservableList; +import seedu.budgeteer.commons.util.CryptoUtil; +import seedu.budgeteer.logic.CommandHistory; +import seedu.budgeteer.model.Model; +import seedu.budgeteer.model.entry.Entry; +import seedu.budgeteer.model.entry.ReportEntryList; + + +/** + * Returns how many Ethereum you can buy at the current market price. + */ +public class EthereumCommand extends Command { + + public static final String COMMAND_WORD = "ethereum"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Displays how much ethereum you can buy.\n" + + "Example: " + COMMAND_WORD; + + public static final String MESSAGE_SUCCESS_HEADER = "You are able to buy "; + + @Override + public CommandResult execute(Model model, CommandHistory history) { + CryptoUtil cryptoUtil = CryptoUtil.getInstance(); + double price = cryptoUtil.getEth(); + + model.updateFilteredEntryList(PREDICATE_SHOW_ALL_ENTRYS); + ObservableList filteredList = model.getFilteredEntryList(); + ReportEntryList reportList = new ReportEntryList(filteredList); + Double total = reportList.getTotal(); + + Double amount = total / price; + amount = (double) Math.round(amount * 100.0) / 100.0; + + String successMessage = MESSAGE_SUCCESS_HEADER + amount.toString() + " ETH."; + + int roundOff = (int) Math.round(price); + + String currentPrice = " The current price of ethereum is $" + roundOff + "."; + successMessage = successMessage + currentPrice; + return new CommandResult(successMessage); + } +} diff --git a/src/main/java/seedu/address/logic/commands/ExitCommand.java b/src/main/java/seedu/budgeteer/logic/commands/ExitCommand.java similarity index 73% rename from src/main/java/seedu/address/logic/commands/ExitCommand.java rename to src/main/java/seedu/budgeteer/logic/commands/ExitCommand.java index 2240a3e4be1f..d289a2a23226 100644 --- a/src/main/java/seedu/address/logic/commands/ExitCommand.java +++ b/src/main/java/seedu/budgeteer/logic/commands/ExitCommand.java @@ -1,7 +1,7 @@ -package seedu.address.logic.commands; +package seedu.budgeteer.logic.commands; -import seedu.address.logic.CommandHistory; -import seedu.address.model.Model; +import seedu.budgeteer.logic.CommandHistory; +import seedu.budgeteer.model.Model; /** * Terminates the program. @@ -14,7 +14,7 @@ public class ExitCommand extends Command { @Override public CommandResult execute(Model model, CommandHistory history) { - return new CommandResult(MESSAGE_EXIT_ACKNOWLEDGEMENT, false, true); + return new CommandResult(MESSAGE_EXIT_ACKNOWLEDGEMENT, false, false, true); } } diff --git a/src/main/java/seedu/budgeteer/logic/commands/ExportExcelCommand.java b/src/main/java/seedu/budgeteer/logic/commands/ExportExcelCommand.java new file mode 100755 index 000000000000..e1fec8811432 --- /dev/null +++ b/src/main/java/seedu/budgeteer/logic/commands/ExportExcelCommand.java @@ -0,0 +1,131 @@ +//@@author ngkaicong +package seedu.budgeteer.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.budgeteer.commons.util.ExcelUtil.setPathFile; +import static seedu.budgeteer.model.Model.PREDICATE_SHOW_ALL_ENTRYS; + +import java.util.List; +import java.util.function.Predicate; + +import org.apache.poi.xssf.usermodel.XSSFSheet; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; + +import seedu.budgeteer.commons.core.Messages; +import seedu.budgeteer.commons.util.ExcelUtil; +import seedu.budgeteer.logic.CommandHistory; +import seedu.budgeteer.model.DirectoryPath; +import seedu.budgeteer.model.Model; +import seedu.budgeteer.model.entry.Date; +import seedu.budgeteer.model.entry.DateIsWithinIntervalPredicate; +import seedu.budgeteer.model.entry.Entry; +import seedu.budgeteer.model.summary.SummaryByDateList; +import seedu.budgeteer.ui.SummaryEntry; + + + +/** + * Export the data of the records within specific period. + */ +public class ExportExcelCommand extends Command { + public static final String COMMAND_WORD = "export"; + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Exports the records within specific period or all records in the Budgeteer into Excel file .\n" + + "Parameters: START_DATE END_DATE DIRECTORY_PATH," + + "START_DATE should be equal to or smaller than END_DATE.\n" + + "You can specifically type what you want to conform. Date/period start with d/ " + + "and Directory path start with dir/.\n" + + "Example 1: You want to export all the records in the Budgeter to Excel file" + + "and store them in the Working directory: " + DirectoryPath.WORKING_DIRECTORY_STRING + + "Example 1: You want to set Directory: " + + COMMAND_WORD + " dir/" + DirectoryPath.WORKING_DIRECTORY_STRING + "\n" + + "Example 2: You want to set records have only 1 date: " + COMMAND_WORD + " d/31-3-1999\n" + + "Example 3: You want to set records whose date lies within the period: " + + COMMAND_WORD + "d/31-3-1999 31-3-2019\n" + + "Example 4: You want to export all the records in the Budgeter: " + COMMAND_WORD + "\n" + + "Example 5: You want to export records lies within the period and set specific Directory: " + + COMMAND_WORD + " d/31-3-1999 31-3-2019" + " dir/" + DirectoryPath.WORKING_DIRECTORY_STRING + "\n"; + public static final int SINGLE_MODE = 1; + public static final int DUO_MODE = 2; + public static final int FIRST_ELEMENT = 0; + public static final int SECOND_ELEMENT = 1; + + private static final int EMPTY_RECORD_LIST_SIZE_EXPORT = 0; + + private Date startDate; + private Date endDate; + private String directoryPath; + private Predicate predicate; + + public ExportExcelCommand() { + this.startDate = null; + this.endDate = null; + this.directoryPath = DirectoryPath.WORKING_DIRECTORY_STRING; + this.predicate = PREDICATE_SHOW_ALL_ENTRYS; + + } + + public ExportExcelCommand(String directoryPath) { + this.startDate = null; + this.endDate = null; + this.directoryPath = directoryPath; + this.predicate = PREDICATE_SHOW_ALL_ENTRYS; + } + + public ExportExcelCommand(Date startDate, Date endDate) { + this.startDate = startDate; + this.endDate = endDate; + this.directoryPath = DirectoryPath.WORKING_DIRECTORY_STRING; + this.predicate = new DateIsWithinIntervalPredicate(startDate, endDate); + } + + public ExportExcelCommand(Date startDate, Date endDate, String directoryPath) { + this.startDate = startDate; + this.endDate = endDate; + this.directoryPath = directoryPath; + this.predicate = new DateIsWithinIntervalPredicate(startDate, endDate); + } + + @Override + public CommandResult execute(Model model, CommandHistory commandHistory) { + requireNonNull(this); + model.updateFilteredEntryList(predicate); + SummaryByDateList summaryList = new SummaryByDateList(model.getFilteredEntryList()); + List entryList = model.getFilteredEntryList(); + List daySummaryEntryList = summaryList.getSummaryList(); + String nameFile = ExcelUtil.setNameExcelFile(startDate, endDate); + String message; + String filePath = setPathFile(nameFile, directoryPath); + + if (exportDataIntoExcelSheetWithGivenRecords(entryList, daySummaryEntryList, filePath)) { + message = String.format(Messages.MESSAGE_EXCEL_FILE_WRITTEN_SUCCESSFULLY, nameFile, directoryPath); + } else { + message = Messages.MESSAGE_EXPORT_COMMAND_ERRORS; + } + return new CommandResult(message); + } + + /** + * Export the records into Excel File. + */ + private static Boolean exportDataIntoExcelSheetWithGivenRecords( + List entryList, List daySummaryEntryList, String filePath) { + XSSFWorkbook workbook = new XSSFWorkbook(); + XSSFSheet recordData = workbook.createSheet("RECORD DATA"); + XSSFSheet summaryData = workbook.createSheet("SUMMARY DATA"); + if (entryList.size() > 0) { + ExcelUtil.writeExcelSheetIntoDirectory( + entryList, daySummaryEntryList, recordData, summaryData, workbook, filePath); + return true; + } + return false; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof ExportExcelCommand // instanceof handles nulls + && predicate.equals(((ExportExcelCommand) other).predicate) + && directoryPath.equals(((ExportExcelCommand) other).directoryPath)); // state check + } +} diff --git a/src/main/java/seedu/budgeteer/logic/commands/FilterCommand.java b/src/main/java/seedu/budgeteer/logic/commands/FilterCommand.java new file mode 100644 index 000000000000..55ed617d6372 --- /dev/null +++ b/src/main/java/seedu/budgeteer/logic/commands/FilterCommand.java @@ -0,0 +1,45 @@ +package seedu.budgeteer.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.function.Predicate; + +import seedu.budgeteer.commons.core.Messages; +import seedu.budgeteer.logic.CommandHistory; +import seedu.budgeteer.model.Model; + +/** + * Filters all entrys in budgeteer book who contain any of the specified argument keywords. + * Keyword matching is case sensitive. + */ +public class FilterCommand extends Command { + public static final String COMMAND_WORD = "filter"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all entrys who contain any of " + + "the specified keywords (non case-sensitive) and displays them as a list with index numbers.\n" + + "Parameters: KEYWORD [MORE_KEYWORDS]...\n" + + "Example: " + COMMAND_WORD + " filter n/John \n" + + "Example: " + COMMAND_WORD + " d/12-01-2019\n" + + "Example: " + COMMAND_WORD + " t/[Friends]"; + + private final Predicate predicate; + + public FilterCommand(Predicate predicate) { + this.predicate = predicate; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) { + requireNonNull(model); + model.updateFilteredEntryList(predicate); + return new CommandResult( + String.format(Messages.MESSAGE_ENTRYS_LISTED_OVERVIEW, model.getFilteredEntryList().size())); + } + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof FilterCommand // instanceof handles nulls + && this.predicate.equals(((FilterCommand) other).predicate)); // state check + } + +} diff --git a/src/main/java/seedu/address/logic/commands/FindCommand.java b/src/main/java/seedu/budgeteer/logic/commands/FindCommand.java similarity index 67% rename from src/main/java/seedu/address/logic/commands/FindCommand.java rename to src/main/java/seedu/budgeteer/logic/commands/FindCommand.java index beb178e3a3f5..8732d8472224 100644 --- a/src/main/java/seedu/address/logic/commands/FindCommand.java +++ b/src/main/java/seedu/budgeteer/logic/commands/FindCommand.java @@ -1,21 +1,21 @@ -package seedu.address.logic.commands; +package seedu.budgeteer.logic.commands; import static java.util.Objects.requireNonNull; -import seedu.address.commons.core.Messages; -import seedu.address.logic.CommandHistory; -import seedu.address.model.Model; -import seedu.address.model.person.NameContainsKeywordsPredicate; +import seedu.budgeteer.commons.core.Messages; +import seedu.budgeteer.logic.CommandHistory; +import seedu.budgeteer.model.Model; +import seedu.budgeteer.model.entry.NameContainsKeywordsPredicate; /** - * Finds and lists all persons in address book whose name contains any of the argument keywords. + * Finds and lists all entrys in budgeteer 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 " + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all entrys 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"; @@ -29,9 +29,9 @@ public FindCommand(NameContainsKeywordsPredicate predicate) { @Override public CommandResult execute(Model model, CommandHistory history) { requireNonNull(model); - model.updateFilteredPersonList(predicate); + model.updateFilteredEntryList(predicate); return new CommandResult( - String.format(Messages.MESSAGE_PERSONS_LISTED_OVERVIEW, model.getFilteredPersonList().size())); + String.format(Messages.MESSAGE_ENTRYS_LISTED_OVERVIEW, model.getFilteredEntryList().size())); } @Override diff --git a/src/main/java/seedu/address/logic/commands/HelpCommand.java b/src/main/java/seedu/budgeteer/logic/commands/HelpCommand.java similarity index 80% rename from src/main/java/seedu/address/logic/commands/HelpCommand.java rename to src/main/java/seedu/budgeteer/logic/commands/HelpCommand.java index f0ef78dddded..aae7a0338a33 100644 --- a/src/main/java/seedu/address/logic/commands/HelpCommand.java +++ b/src/main/java/seedu/budgeteer/logic/commands/HelpCommand.java @@ -1,7 +1,7 @@ -package seedu.address.logic.commands; +package seedu.budgeteer.logic.commands; -import seedu.address.logic.CommandHistory; -import seedu.address.model.Model; +import seedu.budgeteer.logic.CommandHistory; +import seedu.budgeteer.model.Model; /** * Format full help instructions for every command for display. @@ -17,6 +17,6 @@ public class HelpCommand extends Command { @Override public CommandResult execute(Model model, CommandHistory history) { - return new CommandResult(SHOWING_HELP_MESSAGE, true, false); + return new CommandResult(SHOWING_HELP_MESSAGE, true, false, false); } } diff --git a/src/main/java/seedu/address/logic/commands/HistoryCommand.java b/src/main/java/seedu/budgeteer/logic/commands/HistoryCommand.java similarity index 88% rename from src/main/java/seedu/address/logic/commands/HistoryCommand.java rename to src/main/java/seedu/budgeteer/logic/commands/HistoryCommand.java index dc3de1aad55e..8a4dfed38d2a 100644 --- a/src/main/java/seedu/address/logic/commands/HistoryCommand.java +++ b/src/main/java/seedu/budgeteer/logic/commands/HistoryCommand.java @@ -1,12 +1,12 @@ -package seedu.address.logic.commands; +package seedu.budgeteer.logic.commands; import static java.util.Objects.requireNonNull; import java.util.ArrayList; import java.util.Collections; -import seedu.address.logic.CommandHistory; -import seedu.address.model.Model; +import seedu.budgeteer.logic.CommandHistory; +import seedu.budgeteer.model.Model; /** * Lists all the commands entered by user from the start of app launch. diff --git a/src/main/java/seedu/budgeteer/logic/commands/InvestCommand.java b/src/main/java/seedu/budgeteer/logic/commands/InvestCommand.java new file mode 100644 index 000000000000..5dd138e4a206 --- /dev/null +++ b/src/main/java/seedu/budgeteer/logic/commands/InvestCommand.java @@ -0,0 +1,67 @@ +package seedu.budgeteer.logic.commands; + +import static seedu.budgeteer.logic.parser.CliSyntax.PREFIX_INTEREST; +import static seedu.budgeteer.logic.parser.CliSyntax.PREFIX_YEARS; +import static seedu.budgeteer.model.Model.PREDICATE_SHOW_ALL_ENTRYS; + +import javafx.collections.ObservableList; +import seedu.budgeteer.logic.CommandHistory; +import seedu.budgeteer.model.Model; +import seedu.budgeteer.model.entry.Entry; +import seedu.budgeteer.model.entry.Number; +import seedu.budgeteer.model.entry.ReportEntryList; + +/** + * Returns how much stock you can buy at the current market price. + */ +public class InvestCommand extends Command { + + public static final String COMMAND_WORD = "invest"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Calculate long term investments based on current balance.\n" + + "Parameters: " + + PREFIX_INTEREST + "INTEREST RATE " + + PREFIX_YEARS + "YEARS " + + "Example: " + COMMAND_WORD + " " + + PREFIX_INTEREST + "5.5 " + + PREFIX_YEARS + "20 "; + + private final Number num; + + public InvestCommand(Number num) { + this.num = num; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) { + String messageReturn; + + model.updateFilteredEntryList(PREDICATE_SHOW_ALL_ENTRYS); + ObservableList filteredList = model.getFilteredEntryList(); + ReportEntryList reportList = new ReportEntryList(filteredList); + Double total = reportList.getTotal(); + + String[] splitted = num.fullNumber.split("\\s+"); + int interestCount = splitted[0].length() - splitted[0].replace(".", "").length(); + int yearCount = splitted[1].length() - splitted[1].replace(".", "").length(); + if ((interestCount > 1) || (yearCount > 1)) { + messageReturn = "Sorry, you entered an invalid number.\n" + + "Numbers can only have one decimal point."; + } else { + double interestRate = Double.parseDouble(splitted[0]); + double numYears = Double.parseDouble(splitted[1]); + + double compound = total * (Math.pow((1 + interestRate / 100), numYears)); + double investment = (double) Math.round(compound * 100.0) / 100.0; + + String pre = "Your current balance is S$" + total + ".\n"; + String first = "At an interest rate of " + interestRate + "% for " + numYears + " years,"; + String second = " you would have S$" + investment + ".\nCongratulations!"; + + messageReturn = pre + first + second; + } + + return new CommandResult(messageReturn); + } +} diff --git a/src/main/java/seedu/budgeteer/logic/commands/ListCommand.java b/src/main/java/seedu/budgeteer/logic/commands/ListCommand.java new file mode 100644 index 000000000000..2a18419e41db --- /dev/null +++ b/src/main/java/seedu/budgeteer/logic/commands/ListCommand.java @@ -0,0 +1,25 @@ +package seedu.budgeteer.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.budgeteer.model.Model.PREDICATE_SHOW_ALL_ENTRYS; + +import seedu.budgeteer.logic.CommandHistory; +import seedu.budgeteer.model.Model; + +/** + * Lists all entrys in the budgeteer book to the user. + */ +public class ListCommand extends Command { + + public static final String COMMAND_WORD = "list"; + + public static final String MESSAGE_SUCCESS = "Listed all entrys"; + + + @Override + public CommandResult execute(Model model, CommandHistory history) { + requireNonNull(model); + model.updateFilteredEntryList(PREDICATE_SHOW_ALL_ENTRYS); + return new CommandResult(MESSAGE_SUCCESS); + } +} diff --git a/src/main/java/seedu/budgeteer/logic/commands/LitecoinCommand.java b/src/main/java/seedu/budgeteer/logic/commands/LitecoinCommand.java new file mode 100644 index 000000000000..358e9e473272 --- /dev/null +++ b/src/main/java/seedu/budgeteer/logic/commands/LitecoinCommand.java @@ -0,0 +1,46 @@ +package seedu.budgeteer.logic.commands; + +import static seedu.budgeteer.model.Model.PREDICATE_SHOW_ALL_ENTRYS; + +import javafx.collections.ObservableList; +import seedu.budgeteer.commons.util.CryptoUtil; +import seedu.budgeteer.logic.CommandHistory; +import seedu.budgeteer.model.Model; +import seedu.budgeteer.model.entry.Entry; +import seedu.budgeteer.model.entry.ReportEntryList; + + +/** + * Returns how many Litecoin you can buy at the current market price. + */ +public class LitecoinCommand extends Command { + + public static final String COMMAND_WORD = "litecoin"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Displays how much litecoin you can buy.\n" + + "Example: " + COMMAND_WORD; + + public static final String MESSAGE_SUCCESS_HEADER = "You are able to buy "; + + @Override + public CommandResult execute(Model model, CommandHistory history) { + CryptoUtil cryptoUtil = CryptoUtil.getInstance(); + double price = cryptoUtil.getLtc(); + + model.updateFilteredEntryList(PREDICATE_SHOW_ALL_ENTRYS); + ObservableList filteredList = model.getFilteredEntryList(); + ReportEntryList reportList = new ReportEntryList(filteredList); + Double total = reportList.getTotal(); + + Double amount = total / price; + amount = (double) Math.round(amount * 100.0) / 100.0; + + String successMessage = MESSAGE_SUCCESS_HEADER + amount.toString() + " LTC."; + + int roundOff = (int) Math.round(price); + + String currentPrice = " The current price of litecoin is $" + roundOff + "."; + successMessage = successMessage + currentPrice; + return new CommandResult(successMessage); + } +} diff --git a/src/main/java/seedu/budgeteer/logic/commands/LockCommand.java b/src/main/java/seedu/budgeteer/logic/commands/LockCommand.java new file mode 100644 index 000000000000..af717f8b6d09 --- /dev/null +++ b/src/main/java/seedu/budgeteer/logic/commands/LockCommand.java @@ -0,0 +1,111 @@ +package seedu.budgeteer.logic.commands; + +import java.io.IOException; + +import seedu.budgeteer.commons.exceptions.WrongPasswordException; +import seedu.budgeteer.logic.CommandHistory; +import seedu.budgeteer.logic.commands.exceptions.CommandException; +import seedu.budgeteer.model.Model; +import seedu.budgeteer.storage.PasswordManager; + + +/** + * Contain methods to modify the password + */ +public class LockCommand extends Command { + + public static final String COMMAND_WORD = "lock"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Set password and unlock using the password\n" + + "Set Password Parameters:" + COMMAND_WORD + " set/yourchosenpassword\n"; + + public static final String MESSAGE_SUCCESS = "Locked!"; + public static final String MESSAGE_PASSWORD_CHANGE = "Password successfully changed!"; + public static final String MESSAGE_PASSWORD_EXISTS = "Password already exists!"; + public static final String MESSAGE_PASSWORD_REMOVE = "Password removed!"; + public static final String MESSAGE_NO_PASSWORD_EXISTS = "No password!"; + public static final String MESSAGE_WRONG_PASSWORD = "Wrong password!"; + + private final LockMode mode; + + /** + * Creates an LockCommand + */ + public LockCommand(LockMode mode) { + this.mode = mode; + } + + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + try { + return mode.execute(); + } catch (IOException ioe) { + throw new CommandException("Password File not found"); + } + + } + + /** + * Set password if it does not exists + */ + public static class SetLock extends LockMode { + public SetLock(String password) { + super(password); + } + + @Override + public CommandResult execute() throws IOException, CommandException { + if (passExists()) { + throw new CommandException(MESSAGE_PASSWORD_EXISTS); + } else { + PasswordManager.savePassword(getPass()); + return new CommandResult(MESSAGE_SUCCESS); + } + } + } + + /** + * Removes password if it exists + */ + public static class ClearLock extends LockMode { + public ClearLock(String password) { + super(password); + } + + @Override + public CommandResult execute() throws IOException, CommandException { + if (passExists()) { + try { + PasswordManager.removePassword(getPass()); + } catch (WrongPasswordException e) { + throw new CommandException(MESSAGE_WRONG_PASSWORD); + } + return new CommandResult(MESSAGE_PASSWORD_REMOVE); + } else { + throw new CommandException(MESSAGE_NO_PASSWORD_EXISTS); + } + } + } + + /** + * Changes password if it exists + */ + public static class ChangeLock extends LockMode { + + private String newPass; + public ChangeLock(String newPassword) { + super(newPassword); + newPass = newPassword; + } + + @Override + public CommandResult execute() throws IOException { + if (passExists()) { + PasswordManager.savePassword(newPass); + return new CommandResult(MESSAGE_PASSWORD_CHANGE); + } else { + return new CommandResult(MESSAGE_NO_PASSWORD_EXISTS); + } + } + } +} diff --git a/src/main/java/seedu/budgeteer/logic/commands/LockMode.java b/src/main/java/seedu/budgeteer/logic/commands/LockMode.java new file mode 100644 index 000000000000..b5601982f1e1 --- /dev/null +++ b/src/main/java/seedu/budgeteer/logic/commands/LockMode.java @@ -0,0 +1,29 @@ +package seedu.budgeteer.logic.commands; + +import java.io.IOException; + +import seedu.budgeteer.logic.commands.exceptions.CommandException; +import seedu.budgeteer.storage.PasswordManager; + + +/** + * Represents the different modes for LockCommand + */ +public abstract class LockMode { + + private String pass; + + public LockMode(String pass) { + this.pass = pass; + } + + public String getPass() { + return pass; + } + + protected boolean passExists() { + return PasswordManager.passwordExists(); + } + + public abstract CommandResult execute() throws IOException, CommandException; +} diff --git a/src/main/java/seedu/address/logic/commands/RedoCommand.java b/src/main/java/seedu/budgeteer/logic/commands/RedoCommand.java similarity index 61% rename from src/main/java/seedu/address/logic/commands/RedoCommand.java rename to src/main/java/seedu/budgeteer/logic/commands/RedoCommand.java index 227771a4eef6..e16228f4f947 100644 --- a/src/main/java/seedu/address/logic/commands/RedoCommand.java +++ b/src/main/java/seedu/budgeteer/logic/commands/RedoCommand.java @@ -1,14 +1,14 @@ -package seedu.address.logic.commands; +package seedu.budgeteer.logic.commands; import static java.util.Objects.requireNonNull; -import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; +import static seedu.budgeteer.model.Model.PREDICATE_SHOW_ALL_ENTRYS; -import seedu.address.logic.CommandHistory; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.model.Model; +import seedu.budgeteer.logic.CommandHistory; +import seedu.budgeteer.logic.commands.exceptions.CommandException; +import seedu.budgeteer.model.Model; /** - * Reverts the {@code model}'s address book to its previously undone state. + * Reverts the {@code model}'s budgeteer book to its previously undone state. */ public class RedoCommand extends Command { @@ -25,7 +25,7 @@ public CommandResult execute(Model model, CommandHistory history) throws Command } model.redoAddressBook(); - model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + model.updateFilteredEntryList(PREDICATE_SHOW_ALL_ENTRYS); return new CommandResult(MESSAGE_SUCCESS); } } diff --git a/src/main/java/seedu/budgeteer/logic/commands/ReportCommand.java b/src/main/java/seedu/budgeteer/logic/commands/ReportCommand.java new file mode 100644 index 000000000000..c5c8ad96d408 --- /dev/null +++ b/src/main/java/seedu/budgeteer/logic/commands/ReportCommand.java @@ -0,0 +1,52 @@ +package seedu.budgeteer.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.function.Predicate; + +import javafx.collections.ObservableList; + +import seedu.budgeteer.logic.CommandHistory; +import seedu.budgeteer.logic.commands.exceptions.CommandException; +import seedu.budgeteer.model.Model; +import seedu.budgeteer.model.entry.Entry; +import seedu.budgeteer.model.entry.ReportEntryList; + + + +/** + * Filters all entrys in budgeteer book who contain any of the specified argument keywords. + * Keyword matching is case sensitive. + */ +public class ReportCommand extends Command { + public static final String COMMAND_WORD = "report"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Generates a report of the finances incurred " + + "During the specified timeframe. Using the prefix insight/ gives a more detailed report.\n" + + "Parameters: KEYWORD s/[start_date] e/[end_date] [insight/]...\n" + + "Example: " + COMMAND_WORD + " report \n" + + "Example: " + COMMAND_WORD + " report s/21-01-2019\n" + + "Example: " + COMMAND_WORD + " report e/25-03-2019\n" + + "Example: " + COMMAND_WORD + " report s/01-01-2018 e/31-12-2018"; + + private final Predicate predicate; + + public ReportCommand(Predicate predicate) { + this.predicate = predicate; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + requireNonNull(model); + model.updateFilteredEntryList(this.predicate); + ObservableList filteredList = model.getFilteredEntryList(); + ReportEntryList reportList = new ReportEntryList(filteredList); + Double total = reportList.getTotal(); + Double income = reportList.getTotalIncome(); + Double expense = reportList.getTotalExpense(); + return new CommandResult("Overview (Income - Expenses): " + String.format("%.02f", total) + + "\n" + "Total Income: " + String.format("%.02f", income) + + "\n" + "Total Expenses: " + String.format("%.02f", expense), + false, true, false); + } +} diff --git a/src/main/java/seedu/address/logic/commands/SelectCommand.java b/src/main/java/seedu/budgeteer/logic/commands/SelectCommand.java similarity index 51% rename from src/main/java/seedu/address/logic/commands/SelectCommand.java rename to src/main/java/seedu/budgeteer/logic/commands/SelectCommand.java index baa3c1f30bb4..5376e66d0498 100644 --- a/src/main/java/seedu/address/logic/commands/SelectCommand.java +++ b/src/main/java/seedu/budgeteer/logic/commands/SelectCommand.java @@ -1,29 +1,29 @@ -package seedu.address.logic.commands; +package seedu.budgeteer.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.CommandHistory; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.model.Model; -import seedu.address.model.person.Person; +import seedu.budgeteer.commons.core.Messages; +import seedu.budgeteer.commons.core.index.Index; +import seedu.budgeteer.logic.CommandHistory; +import seedu.budgeteer.logic.commands.exceptions.CommandException; +import seedu.budgeteer.model.Model; +import seedu.budgeteer.model.entry.Entry; /** - * Selects a person identified using it's displayed index from the address book. + * Selects a entry identified using it's displayed index from the budgeteer book. */ public class SelectCommand extends Command { public static final String COMMAND_WORD = "select"; public static final String MESSAGE_USAGE = COMMAND_WORD - + ": Selects the person identified by the index number used in the displayed person list.\n" + + ": Selects the entry identified by the index number used in the displayed entry list.\n" + "Parameters: INDEX (must be a positive integer)\n" + "Example: " + COMMAND_WORD + " 1"; - public static final String MESSAGE_SELECT_PERSON_SUCCESS = "Selected Person: %1$s"; + public static final String MESSAGE_SELECT_ENTRY_SUCCESS = "Selected Entry: %1$s"; private final Index targetIndex; @@ -35,14 +35,14 @@ public SelectCommand(Index targetIndex) { public CommandResult execute(Model model, CommandHistory history) throws CommandException { requireNonNull(model); - List filteredPersonList = model.getFilteredPersonList(); + List filteredEntryList = model.getFilteredEntryList(); - if (targetIndex.getZeroBased() >= filteredPersonList.size()) { - throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + if (targetIndex.getZeroBased() >= filteredEntryList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_ENTRY_DISPLAYED_INDEX); } - model.setSelectedPerson(filteredPersonList.get(targetIndex.getZeroBased())); - return new CommandResult(String.format(MESSAGE_SELECT_PERSON_SUCCESS, targetIndex.getOneBased())); + model.setSelectedEntry(filteredEntryList.get(targetIndex.getZeroBased())); + return new CommandResult(String.format(MESSAGE_SELECT_ENTRY_SUCCESS, targetIndex.getOneBased())); } diff --git a/src/main/java/seedu/budgeteer/logic/commands/StockCommand.java b/src/main/java/seedu/budgeteer/logic/commands/StockCommand.java new file mode 100644 index 000000000000..859b03dc4414 --- /dev/null +++ b/src/main/java/seedu/budgeteer/logic/commands/StockCommand.java @@ -0,0 +1,113 @@ +package seedu.budgeteer.logic.commands; + +import static seedu.budgeteer.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.budgeteer.model.Model.PREDICATE_SHOW_ALL_ENTRYS; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; + +import javafx.collections.ObservableList; +import seedu.budgeteer.logic.CommandHistory; +import seedu.budgeteer.model.Model; +import seedu.budgeteer.model.entry.Entry; +import seedu.budgeteer.model.entry.Name; +import seedu.budgeteer.model.entry.ReportEntryList; + +/** + * Returns how much stock you can buy at the current market price. + */ +public class StockCommand extends Command { + + public static final String COMMAND_WORD = "stock"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Displays how much of a certain stock you can buy.\n" + + "Parameters: " + + PREFIX_NAME + "NAME " + + "Example: " + COMMAND_WORD + " " + + PREFIX_NAME + "STOCK NAME " + + "Example: " + COMMAND_WORD + " " + + PREFIX_NAME + "MSFT"; + + private String firstUrl = "https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol="; + private String secondUrl = "&apikey=Y6G36I3BIPQL5I2"; + + private final Name name; + + public StockCommand(Name name) { + this.name = name; + } + + /** + * Function that calls the stock API and returns the JSON in string format + */ + public String stockPrice() { + String ret = ""; + try { + String temp = firstUrl + name.fullName.toUpperCase() + secondUrl; + + URL url = new URL(temp); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("GET"); + conn.setRequestProperty("Accept", "application/json"); + + if (conn.getResponseCode() != 200) { + throw new RuntimeException("Failed : HTTP error code : " + + conn.getResponseCode()); + } + + BufferedReader br = new BufferedReader(new InputStreamReader((conn.getInputStream()))); + + String output; + int lineNumber = 7; + while (lineNumber > 0) { + output = br.readLine(); + ret = output; + lineNumber -= 1; + } + + conn.disconnect(); + + } catch (MalformedURLException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + return ret; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) { + double price; + String messageReturn; + + model.updateFilteredEntryList(PREDICATE_SHOW_ALL_ENTRYS); + ObservableList filteredList = model.getFilteredEntryList(); + ReportEntryList reportList = new ReportEntryList(filteredList); + Double total = reportList.getTotal(); + + String full = stockPrice(); + + if (full == null) { + messageReturn = "Sorry, your input is not a valid stock. Please try again."; + } else if (full.length() < 30) { + messageReturn = "Sorry, your input is not a valid stock. Please try again."; + } else { + price = Float.parseFloat(full.substring(22, 28)); + Double printPrice = (double) Math.round(price * 100.0) / 100.0; + + String first = "You are able to buy "; + Double amount = total / price; + amount = (double) Math.round(amount * 100.0) / 100.0; + String second = first + amount + " " + name.fullName.toUpperCase() + " stock. "; + + messageReturn = second + "The price of the stock " + name.fullName.toUpperCase() + + " is $" + printPrice.toString() + "."; + } + + return new CommandResult(messageReturn); + } +} diff --git a/src/main/java/seedu/address/logic/commands/UndoCommand.java b/src/main/java/seedu/budgeteer/logic/commands/UndoCommand.java similarity index 62% rename from src/main/java/seedu/address/logic/commands/UndoCommand.java rename to src/main/java/seedu/budgeteer/logic/commands/UndoCommand.java index 40441264f346..48311026ada7 100644 --- a/src/main/java/seedu/address/logic/commands/UndoCommand.java +++ b/src/main/java/seedu/budgeteer/logic/commands/UndoCommand.java @@ -1,14 +1,14 @@ -package seedu.address.logic.commands; +package seedu.budgeteer.logic.commands; import static java.util.Objects.requireNonNull; -import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; +import static seedu.budgeteer.model.Model.PREDICATE_SHOW_ALL_ENTRYS; -import seedu.address.logic.CommandHistory; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.model.Model; +import seedu.budgeteer.logic.CommandHistory; +import seedu.budgeteer.logic.commands.exceptions.CommandException; +import seedu.budgeteer.model.Model; /** - * Reverts the {@code model}'s address book to its previous state. + * Reverts the {@code model}'s budgeteer book to its previous state. */ public class UndoCommand extends Command { @@ -25,7 +25,7 @@ public CommandResult execute(Model model, CommandHistory history) throws Command } model.undoAddressBook(); - model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + model.updateFilteredEntryList(PREDICATE_SHOW_ALL_ENTRYS); return new CommandResult(MESSAGE_SUCCESS); } } diff --git a/src/main/java/seedu/address/logic/commands/exceptions/CommandException.java b/src/main/java/seedu/budgeteer/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/seedu/budgeteer/logic/commands/exceptions/CommandException.java index a16bd14f2cde..5a74cde2a9c5 100644 --- a/src/main/java/seedu/address/logic/commands/exceptions/CommandException.java +++ b/src/main/java/seedu/budgeteer/logic/commands/exceptions/CommandException.java @@ -1,4 +1,4 @@ -package seedu.address.logic.commands.exceptions; +package seedu.budgeteer.logic.commands.exceptions; /** * Represents an error which occurs during execution of a {@link Command}. diff --git a/src/main/java/seedu/address/logic/parser/AddCommandParser.java b/src/main/java/seedu/budgeteer/logic/parser/AddCommandParser.java similarity index 53% rename from src/main/java/seedu/address/logic/parser/AddCommandParser.java rename to src/main/java/seedu/budgeteer/logic/parser/AddCommandParser.java index 3b8bfa035e83..5e42c5cfd763 100644 --- a/src/main/java/seedu/address/logic/parser/AddCommandParser.java +++ b/src/main/java/seedu/budgeteer/logic/parser/AddCommandParser.java @@ -1,23 +1,21 @@ -package seedu.address.logic.parser; +package seedu.budgeteer.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 static seedu.budgeteer.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.budgeteer.logic.parser.CliSyntax.PREFIX_CASHFLOW; +import static seedu.budgeteer.logic.parser.CliSyntax.PREFIX_DATE; +import static seedu.budgeteer.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.budgeteer.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; +import seedu.budgeteer.logic.commands.AddCommand; +import seedu.budgeteer.logic.parser.exceptions.ParseException; +import seedu.budgeteer.model.entry.CashFlow; +import seedu.budgeteer.model.entry.Date; +import seedu.budgeteer.model.entry.Entry; +import seedu.budgeteer.model.entry.Name; +import seedu.budgeteer.model.tag.Tag; /** * Parses input arguments and creates a new AddCommand object @@ -31,20 +29,19 @@ public class AddCommandParser implements Parser { */ public AddCommand parse(String args) throws ParseException { ArgumentMultimap argMultimap = - ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG); + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_DATE, PREFIX_CASHFLOW, PREFIX_TAG); - if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_ADDRESS, PREFIX_PHONE, PREFIX_EMAIL) + if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_DATE, PREFIX_CASHFLOW) || !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()); + Date date = ParserUtil.parseDate(argMultimap.getValue(PREFIX_DATE).get()); + CashFlow cashFlow = ParserUtil.parseCashFlow(argMultimap.getValue(PREFIX_CASHFLOW).get()); Set tagList = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG)); - Person person = new Person(name, phone, email, address, tagList); + Entry person = new Entry(name, date, cashFlow, tagList); return new AddCommand(person); } diff --git a/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java b/src/main/java/seedu/budgeteer/logic/parser/ArgumentMultimap.java similarity index 98% rename from src/main/java/seedu/address/logic/parser/ArgumentMultimap.java rename to src/main/java/seedu/budgeteer/logic/parser/ArgumentMultimap.java index 954c8e18f8ea..1d13a50f6c1a 100644 --- a/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java +++ b/src/main/java/seedu/budgeteer/logic/parser/ArgumentMultimap.java @@ -1,4 +1,4 @@ -package seedu.address.logic.parser; +package seedu.budgeteer.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/seedu/budgeteer/logic/parser/ArgumentTokenizer.java similarity index 99% rename from src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java rename to src/main/java/seedu/budgeteer/logic/parser/ArgumentTokenizer.java index 5c9aebfa4888..1b308006980e 100644 --- a/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java +++ b/src/main/java/seedu/budgeteer/logic/parser/ArgumentTokenizer.java @@ -1,4 +1,4 @@ -package seedu.address.logic.parser; +package seedu.budgeteer.logic.parser; import java.util.ArrayList; import java.util.Arrays; diff --git a/src/main/java/seedu/budgeteer/logic/parser/BitcoinCommandParser.java b/src/main/java/seedu/budgeteer/logic/parser/BitcoinCommandParser.java new file mode 100644 index 000000000000..534ead96a840 --- /dev/null +++ b/src/main/java/seedu/budgeteer/logic/parser/BitcoinCommandParser.java @@ -0,0 +1,20 @@ +package seedu.budgeteer.logic.parser; + +import seedu.budgeteer.logic.commands.BitcoinCommand; +import seedu.budgeteer.logic.parser.exceptions.ParseException; + + +/** + * Parses input arguments and creates a new FindCommand object + */ +public class BitcoinCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the ReportCommand + * and returns an ReportCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public BitcoinCommand parse(String args) throws ParseException { + return new BitcoinCommand(); + } + +} diff --git a/src/main/java/seedu/budgeteer/logic/parser/CliSyntax.java b/src/main/java/seedu/budgeteer/logic/parser/CliSyntax.java new file mode 100644 index 000000000000..d4c86db3708d --- /dev/null +++ b/src/main/java/seedu/budgeteer/logic/parser/CliSyntax.java @@ -0,0 +1,27 @@ +package seedu.budgeteer.logic.parser; + +/** + * Contains Command Line Interface (CLI) syntax definitions common to multiple commands + */ +public class CliSyntax { + + /* Prefix definitions */ + public static final Prefix PREFIX_CASHFLOW = new Prefix("c/"); + public static final Prefix PREFIX_DATE = new Prefix("d/"); + public static final Prefix PREFIX_NAME = new Prefix("n/"); + public static final Prefix PREFIX_TAG = new Prefix("t/"); + + public static final Prefix PREFIX_INTEREST = new Prefix("interest/"); + public static final Prefix PREFIX_YEARS = new Prefix("years/"); + + public static final Prefix PREFIX_STARTDATE = new Prefix("s/"); + public static final Prefix PREFIX_ENDDATE = new Prefix("e/"); + public static final Prefix PREFIX_INSIGHT = new Prefix("insight/"); + + public static final Prefix PREFIX_SET = new Prefix("set/"); + public static final Prefix PREFIX_CHANGE = new Prefix("change/"); + public static final Prefix PREFIX_REMOVE = new Prefix("remove/"); + public static final Prefix PREFIX_DIR = new Prefix("dir/"); + + +} diff --git a/src/main/java/seedu/budgeteer/logic/parser/CryptoCommandParser.java b/src/main/java/seedu/budgeteer/logic/parser/CryptoCommandParser.java new file mode 100644 index 000000000000..fde8adb542fb --- /dev/null +++ b/src/main/java/seedu/budgeteer/logic/parser/CryptoCommandParser.java @@ -0,0 +1,43 @@ +package seedu.budgeteer.logic.parser; + +import static seedu.budgeteer.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.budgeteer.logic.parser.CliSyntax.PREFIX_NAME; + +import java.util.stream.Stream; + +import seedu.budgeteer.logic.commands.CryptoCommand; +import seedu.budgeteer.logic.parser.exceptions.ParseException; +import seedu.budgeteer.model.entry.Name; + +/** + * Parses input arguments and creates a new FindCommand object + */ +public class CryptoCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the FindCommand + * and returns an FindCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public CryptoCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_NAME); + + if (!arePrefixesPresent(argMultimap, PREFIX_NAME) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, CryptoCommand.MESSAGE_USAGE)); + } + + Name name = ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get()); + + return new CryptoCommand(name); + } + + /** + * 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/DeleteCommandParser.java b/src/main/java/seedu/budgeteer/logic/parser/DeleteCommandParser.java similarity index 72% rename from src/main/java/seedu/address/logic/parser/DeleteCommandParser.java rename to src/main/java/seedu/budgeteer/logic/parser/DeleteCommandParser.java index 4d1f4bb0e4ec..a8abc813246b 100644 --- a/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java +++ b/src/main/java/seedu/budgeteer/logic/parser/DeleteCommandParser.java @@ -1,10 +1,10 @@ -package seedu.address.logic.parser; +package seedu.budgeteer.logic.parser; -import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.budgeteer.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; +import seedu.budgeteer.commons.core.index.Index; +import seedu.budgeteer.logic.commands.DeleteCommand; +import seedu.budgeteer.logic.parser.exceptions.ParseException; /** * Parses input arguments and creates a new DeleteCommand object diff --git a/src/main/java/seedu/budgeteer/logic/parser/DisplayCommandParser.java b/src/main/java/seedu/budgeteer/logic/parser/DisplayCommandParser.java new file mode 100644 index 000000000000..87fc4f86f758 --- /dev/null +++ b/src/main/java/seedu/budgeteer/logic/parser/DisplayCommandParser.java @@ -0,0 +1,70 @@ +package seedu.budgeteer.logic.parser; + +import static seedu.budgeteer.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.budgeteer.logic.commands.DisplayCommand.ASCENDING_CONDITION; +import static seedu.budgeteer.logic.commands.DisplayCommand.CATEGORY_AND_ORDER_SPECIFIED; +import static seedu.budgeteer.logic.commands.DisplayCommand.CATEGORY_NAME; +import static seedu.budgeteer.logic.commands.DisplayCommand.CATEGORY_SET; +import static seedu.budgeteer.logic.commands.DisplayCommand.DESCENDING_CONDITION; +import static seedu.budgeteer.logic.commands.DisplayCommand.ONLY_CATEGORY_OR_ORDER_SPECIFIED; +import static seedu.budgeteer.logic.commands.DisplayCommand.ORDER_SET; + +import seedu.budgeteer.logic.commands.DisplayCommand; +import seedu.budgeteer.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new DisplayCommand object + */ +public class DisplayCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the DisplayCommand + * and returns a DisplayCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public DisplayCommand parse(String args) throws ParseException { + + String trimmedArgs = args.trim(); + if (trimmedArgs.isEmpty()) { + throw new ParseException((String.format(MESSAGE_INVALID_COMMAND_FORMAT, DisplayCommand.MESSAGE_USAGE))); + } + + String category; + Boolean ascending; + + String[] argList = trimmedArgs.split("\\s+"); + + if ((argList.length) == ONLY_CATEGORY_OR_ORDER_SPECIFIED) { + if (!CATEGORY_SET.contains(argList[0].toLowerCase()) && !ORDER_SET.contains(argList[0].toLowerCase())) { + throw new ParseException((String.format(MESSAGE_INVALID_COMMAND_FORMAT, DisplayCommand.MESSAGE_USAGE))); + } + category = argList[0].toLowerCase(); + ascending = true; + if (category.equals(DESCENDING_CONDITION)) { + category = CATEGORY_NAME; + ascending = false; + } else if (category.equals(ASCENDING_CONDITION)) { + category = CATEGORY_NAME; + } + } else if (argList.length == CATEGORY_AND_ORDER_SPECIFIED) { + if ((ORDER_SET.contains(argList[0].toLowerCase()) || ORDER_SET.contains(argList[1].toLowerCase())) + && (CATEGORY_SET.contains(argList[0].toLowerCase()) + || CATEGORY_SET.contains(argList[1].toLowerCase()))) { + if (ORDER_SET.contains(argList[0].toLowerCase())) { + ascending = !(argList[0].toLowerCase().equals(DESCENDING_CONDITION)); + category = argList[1].toLowerCase(); + } else { + ascending = (argList[1].toLowerCase().equals(ASCENDING_CONDITION)); + category = argList[0].toLowerCase(); + } + } else { + throw new ParseException((String.format(MESSAGE_INVALID_COMMAND_FORMAT, DisplayCommand.MESSAGE_USAGE))); + } + } else { + throw new ParseException((String.format(MESSAGE_INVALID_COMMAND_FORMAT, DisplayCommand.MESSAGE_USAGE))); + } + + return new DisplayCommand(category, ascending); + } + +} diff --git a/src/main/java/seedu/address/logic/parser/EditCommandParser.java b/src/main/java/seedu/budgeteer/logic/parser/EditCommandParser.java similarity index 53% rename from src/main/java/seedu/address/logic/parser/EditCommandParser.java rename to src/main/java/seedu/budgeteer/logic/parser/EditCommandParser.java index 845644b7dea1..f02b770146ca 100644 --- a/src/main/java/seedu/address/logic/parser/EditCommandParser.java +++ b/src/main/java/seedu/budgeteer/logic/parser/EditCommandParser.java @@ -1,23 +1,22 @@ -package seedu.address.logic.parser; +package seedu.budgeteer.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 static seedu.budgeteer.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.budgeteer.logic.parser.CliSyntax.PREFIX_CASHFLOW; +import static seedu.budgeteer.logic.parser.CliSyntax.PREFIX_DATE; +import static seedu.budgeteer.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.budgeteer.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; +import seedu.budgeteer.commons.core.index.Index; +import seedu.budgeteer.logic.commands.EditCommand; +import seedu.budgeteer.logic.commands.EditCommand.EditEntryDescriptor; +import seedu.budgeteer.logic.parser.exceptions.ParseException; +import seedu.budgeteer.model.tag.Tag; /** * Parses input arguments and creates a new EditCommand object @@ -32,7 +31,7 @@ public class EditCommandParser implements Parser { public EditCommand parse(String args) throws ParseException { requireNonNull(args); ArgumentMultimap argMultimap = - ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG); + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_DATE, PREFIX_CASHFLOW, PREFIX_TAG); Index index; @@ -42,26 +41,23 @@ public EditCommand parse(String args) throws ParseException { throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE), pe); } - EditPersonDescriptor editPersonDescriptor = new EditPersonDescriptor(); + EditEntryDescriptor editEntryDescriptor = new EditEntryDescriptor(); if (argMultimap.getValue(PREFIX_NAME).isPresent()) { - editPersonDescriptor.setName(ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get())); + editEntryDescriptor.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_DATE).isPresent()) { + editEntryDescriptor.setDate(ParserUtil.parseDate(argMultimap.getValue(PREFIX_DATE).get())); } - if (argMultimap.getValue(PREFIX_EMAIL).isPresent()) { - editPersonDescriptor.setEmail(ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get())); + if (argMultimap.getValue(PREFIX_CASHFLOW).isPresent()) { + editEntryDescriptor.setCashFlow(ParserUtil.parseCashFlow(argMultimap.getValue(PREFIX_CASHFLOW).get())); } - if (argMultimap.getValue(PREFIX_ADDRESS).isPresent()) { - editPersonDescriptor.setAddress(ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get())); - } - parseTagsForEdit(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(editPersonDescriptor::setTags); + parseTagsForEdit(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(editEntryDescriptor::setTags); - if (!editPersonDescriptor.isAnyFieldEdited()) { + if (!editEntryDescriptor.isAnyFieldEdited()) { throw new ParseException(EditCommand.MESSAGE_NOT_EDITED); } - return new EditCommand(index, editPersonDescriptor); + return new EditCommand(index, editEntryDescriptor); } /** diff --git a/src/main/java/seedu/budgeteer/logic/parser/EntriesBookParser.java b/src/main/java/seedu/budgeteer/logic/parser/EntriesBookParser.java new file mode 100644 index 000000000000..2690ecabbc39 --- /dev/null +++ b/src/main/java/seedu/budgeteer/logic/parser/EntriesBookParser.java @@ -0,0 +1,137 @@ +package seedu.budgeteer.logic.parser; + +import static seedu.budgeteer.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.budgeteer.commons.core.Messages.MESSAGE_UNKNOWN_COMMAND; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import seedu.budgeteer.logic.commands.AddCommand; +import seedu.budgeteer.logic.commands.BitcoinCommand; +import seedu.budgeteer.logic.commands.ClearCommand; +import seedu.budgeteer.logic.commands.Command; +import seedu.budgeteer.logic.commands.CryptoCommand; +import seedu.budgeteer.logic.commands.DeleteCommand; +import seedu.budgeteer.logic.commands.DisplayCommand; +import seedu.budgeteer.logic.commands.EditCommand; +import seedu.budgeteer.logic.commands.EthereumCommand; +import seedu.budgeteer.logic.commands.ExitCommand; +import seedu.budgeteer.logic.commands.ExportExcelCommand; +import seedu.budgeteer.logic.commands.FilterCommand; +import seedu.budgeteer.logic.commands.FindCommand; +import seedu.budgeteer.logic.commands.HelpCommand; +import seedu.budgeteer.logic.commands.HistoryCommand; +import seedu.budgeteer.logic.commands.InvestCommand; +import seedu.budgeteer.logic.commands.ListCommand; +import seedu.budgeteer.logic.commands.LitecoinCommand; +import seedu.budgeteer.logic.commands.LockCommand; +import seedu.budgeteer.logic.commands.RedoCommand; +import seedu.budgeteer.logic.commands.ReportCommand; +import seedu.budgeteer.logic.commands.SelectCommand; +import seedu.budgeteer.logic.commands.StockCommand; +import seedu.budgeteer.logic.commands.UndoCommand; +import seedu.budgeteer.logic.parser.exceptions.ParseException; + +/** + * Parses user input. + */ +public class EntriesBookParser { + + /** + * 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 SelectCommand.COMMAND_WORD: + return new SelectCommandParser().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 DisplayCommand.COMMAND_WORD: + return new DisplayCommandParser().parse(arguments); + + case HistoryCommand.COMMAND_WORD: + return new HistoryCommand(); + + case BitcoinCommand.COMMAND_WORD: + return new BitcoinCommandParser().parse(arguments); + + case EthereumCommand.COMMAND_WORD: + return new EthereumCommandParser().parse(arguments); + + case LitecoinCommand.COMMAND_WORD: + return new LitecoinCommandParser().parse(arguments); + + case StockCommand.COMMAND_WORD: + return new StockCommandParser().parse(arguments); + + case CryptoCommand.COMMAND_WORD: + return new CryptoCommandParser().parse(arguments); + + case InvestCommand.COMMAND_WORD: + return new InvestCommandParser().parse(arguments); + + case ExitCommand.COMMAND_WORD: + return new ExitCommand(); + + case HelpCommand.COMMAND_WORD: + return new HelpCommand(); + + case UndoCommand.COMMAND_WORD: + return new UndoCommand(); + + case RedoCommand.COMMAND_WORD: + return new RedoCommand(); + + case FilterCommand.COMMAND_WORD: + return new FilterCommandParser().parse(arguments); + + case ReportCommand.COMMAND_WORD: + return new ReportCommandParser().parse(arguments); + + case LockCommand.COMMAND_WORD: + return new LockCommandParser().parse(arguments); + + case ExportExcelCommand.COMMAND_WORD: + return new ExportExcelCommand(); + + default: + throw new ParseException(MESSAGE_UNKNOWN_COMMAND); + } + } + +} diff --git a/src/main/java/seedu/budgeteer/logic/parser/EthereumCommandParser.java b/src/main/java/seedu/budgeteer/logic/parser/EthereumCommandParser.java new file mode 100644 index 000000000000..fcd36f1053eb --- /dev/null +++ b/src/main/java/seedu/budgeteer/logic/parser/EthereumCommandParser.java @@ -0,0 +1,20 @@ +package seedu.budgeteer.logic.parser; + +import seedu.budgeteer.logic.commands.EthereumCommand; +import seedu.budgeteer.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new FindCommand object + */ +public class EthereumCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the ReportCommand + * and returns an ReportCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public EthereumCommand parse(String args) throws ParseException { + return new EthereumCommand(); + } + + +} diff --git a/src/main/java/seedu/budgeteer/logic/parser/ExportExcelCommandParser.java b/src/main/java/seedu/budgeteer/logic/parser/ExportExcelCommandParser.java new file mode 100755 index 000000000000..45be9fac01b9 --- /dev/null +++ b/src/main/java/seedu/budgeteer/logic/parser/ExportExcelCommandParser.java @@ -0,0 +1,143 @@ +//@@author ngkaicong +package seedu.budgeteer.logic.parser; + +import static seedu.budgeteer.commons.util.DateUtil.isEarlierThan; +import static seedu.budgeteer.logic.parser.CliSyntax.PREFIX_DATE; +import static seedu.budgeteer.logic.parser.CliSyntax.PREFIX_DIR; + +import java.util.Arrays; +import java.util.stream.Stream; + +import seedu.budgeteer.commons.core.Messages; +import seedu.budgeteer.logic.commands.ExportExcelCommand; +import seedu.budgeteer.logic.parser.exceptions.ParseException; +import seedu.budgeteer.model.entry.Date; + + + + + +/** + * Parses input arguments and create ExportExcelCommand object. + */ +public class ExportExcelCommandParser implements Parser { + private static String whiteSpace = " "; + + /** + * Parses the given code {@code String} of arguments in the context of the ExportExcelCommand + * @return an ExportExcelCommand object for execution. + * @throws ParseException if the user input does not conform the expected format. + */ + public ExportExcelCommand parse(String args) throws ParseException { + String trimmedArgs = args.trim(); + if (trimmedArgs.isEmpty()) { + return new ExportExcelCommand(); + } + return createExportExcelCommand(args); + } + + /** + * Create Export Excel Command using args. + */ + private static ExportExcelCommand createExportExcelCommand (String args) throws ParseException { + String stringDate = whiteSpace; + String stringPath = whiteSpace; + boolean isDateExist = true; + boolean isPathExist = true; + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_DATE, PREFIX_DIR); + if (!arePrefixesPresent(argMultimap, PREFIX_DATE) || !argMultimap.getPreamble().isEmpty()) { + isDateExist = false; + } + if (!arePrefixesPresent(argMultimap, PREFIX_DIR) || !argMultimap.getPreamble().isEmpty()) { + isPathExist = false; + } + if (!isDateExist && !isPathExist) { + throw new ParseException( + String.format(Messages.MESSAGE_INVALID_COMMAND_FORMAT, ExportExcelCommand.MESSAGE_USAGE)); + } + if (isDateExist) { + stringDate = argMultimap.getValue(PREFIX_DATE).get(); + } + if (isPathExist) { + stringPath = argMultimap.getValue(PREFIX_DIR).get(); + } + if (stringDate.trim().isEmpty() && stringPath.trim().isEmpty()) { + return new ExportExcelCommand(); + } + return parseArgumentsModeIntoCommand(stringDate.trim(), stringPath.trim()); + } + /** + * Parse the arguments into different argument mode, hence, we will have different command mode. + */ + private static ExportExcelCommand parseArgumentsModeIntoCommand (String stringDate, String stringPath) + throws ParseException { + String directoryPath; + if (stringDate.isEmpty()) { + directoryPath = ParserUtil.parseDirectoryString(stringPath); + return new ExportExcelCommand(directoryPath); + } else { + String[] dates = splitByWhitespace(stringDate); + return parseDateIntoDifferentMode(dates, stringPath); + } + } + + /** + * Parse the string Date into different mode, hence return different commands. + */ + private static ExportExcelCommand parseDateIntoDifferentMode (String[] dates, String stringPath) + throws ParseException { + Date startDate; + Date endDate; + String directoryPath; + int dateNum = Arrays.asList(dates).size(); + + if (dateNum > ExportExcelCommand.DUO_MODE) { + throw new ParseException( + String.format(Messages.MESSAGE_INVALID_COMMAND_FORMAT, Messages.MESSAGE_INVALID_DATE_REQUIRED)); + } else if (dateNum == ExportExcelCommand.SINGLE_MODE) { + startDate = ParserUtil.parseDate(Arrays.asList(dates).get(ExportExcelCommand.FIRST_ELEMENT).trim()); + endDate = ParserUtil.parseDate(Arrays.asList(dates).get(ExportExcelCommand.FIRST_ELEMENT).trim()); + } else { + startDate = ParserUtil.parseDate(Arrays.asList(dates).get(ExportExcelCommand.FIRST_ELEMENT).trim()); + endDate = ParserUtil.parseDate(Arrays.asList(dates).get(ExportExcelCommand.SECOND_ELEMENT).trim()); + } + if (isDateOrderValid(startDate, endDate)) { + if (stringPath == null || stringPath.isEmpty()) { + return new ExportExcelCommand(startDate, endDate); + } else { + directoryPath = ParserUtil.parseDirectoryString(stringPath); + return new ExportExcelCommand(startDate, endDate, directoryPath); + } + } else { + throw new ParseException( + String.format(Messages.MESSAGE_INVALID_COMMAND_FORMAT, Messages.MESSAGE_INVALID_STARTDATE_ENDDATE)); + } + } + + /** + * 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()); + } + + /** + * Splits a string using whitespace as delimiters + * @param args String arguments that have 2 dates. + * @return array of split strings + */ + private static String[] splitByWhitespace(String args) { + if (args.isEmpty()) { + return null; + } + return args.split("\\s+"); + } + + /** + * Check whether the Dates are valid period or not. + */ + private static boolean isDateOrderValid(Date startDate, Date endDate) { + return isEarlierThan(startDate, endDate) || startDate.equals(endDate); + } +} diff --git a/src/main/java/seedu/budgeteer/logic/parser/FilterCommandParser.java b/src/main/java/seedu/budgeteer/logic/parser/FilterCommandParser.java new file mode 100644 index 000000000000..4392c8f3c72f --- /dev/null +++ b/src/main/java/seedu/budgeteer/logic/parser/FilterCommandParser.java @@ -0,0 +1,91 @@ +package seedu.budgeteer.logic.parser; + +import static seedu.budgeteer.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.budgeteer.logic.parser.CliSyntax.PREFIX_CASHFLOW; +import static seedu.budgeteer.logic.parser.CliSyntax.PREFIX_DATE; +import static seedu.budgeteer.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.budgeteer.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.budgeteer.model.Model.PREDICATE_SHOW_ALL_ENTRYS; + +import java.util.Arrays; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import seedu.budgeteer.logic.commands.FilterCommand; +import seedu.budgeteer.logic.parser.exceptions.ParseException; +import seedu.budgeteer.model.entry.CashFlowContainsSpecifiedKeywordsPredicate; +import seedu.budgeteer.model.entry.DateContainsSpecifiedKeywordsPredicate; +import seedu.budgeteer.model.entry.NameContainsKeywordsPredicate; +import seedu.budgeteer.model.entry.TagContainsSpecifiedKeywordsPredicate; + +/** + * Parses input arguments and creates a new FindCommand object + */ +public class FilterCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the FilterCommand + * and returns an FilterCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public FilterCommand parse(String args) throws ParseException { + + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_DATE, PREFIX_CASHFLOW, PREFIX_TAG); + + if (!arePrefixesPresent(argMultimap, PREFIX_NAME) + && !arePrefixesPresent(argMultimap, PREFIX_DATE) + && !arePrefixesPresent(argMultimap, PREFIX_CASHFLOW) + && !arePrefixesPresent(argMultimap, PREFIX_TAG) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, FilterCommand.MESSAGE_USAGE)); + } + + Predicate finalPredicate = PREDICATE_SHOW_ALL_ENTRYS; + if (arePrefixesPresent(argMultimap, PREFIX_NAME)) { + String arguments = argMultimap.getValue(PREFIX_NAME).get(); + if (arguments.equalsIgnoreCase("")) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, FilterCommand.MESSAGE_USAGE)); + } + String[] keyWords = arguments.split("\\s+"); + finalPredicate = (new NameContainsKeywordsPredicate(Arrays.asList(keyWords))); + } + + if (arePrefixesPresent(argMultimap, PREFIX_DATE)) { + String arguments = argMultimap.getValue(PREFIX_DATE).get(); + if (arguments.equalsIgnoreCase("")) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, FilterCommand.MESSAGE_USAGE)); + } + String[] keyWords = arguments.split("\\s+"); + finalPredicate = (new DateContainsSpecifiedKeywordsPredicate(Arrays.asList(keyWords))); + } + + if (arePrefixesPresent(argMultimap, PREFIX_CASHFLOW)) { + String arguments = argMultimap.getValue(PREFIX_CASHFLOW).get(); + if (arguments.equalsIgnoreCase("")) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, FilterCommand.MESSAGE_USAGE)); + } + String[] keyWords = arguments.split("\\s+"); + finalPredicate = (new CashFlowContainsSpecifiedKeywordsPredicate(Arrays.asList(keyWords))); + } + + if (arePrefixesPresent(argMultimap, PREFIX_TAG)) { + String arguments = argMultimap.getValue(PREFIX_TAG).get(); + if (arguments.equalsIgnoreCase("")) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, FilterCommand.MESSAGE_USAGE)); + } + String[] keyWords = arguments.split("\\s+"); + finalPredicate = (new TagContainsSpecifiedKeywordsPredicate(Arrays.asList(keyWords))); + } + + return new FilterCommand(finalPredicate); + } + + /** + * 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/FindCommandParser.java b/src/main/java/seedu/budgeteer/logic/parser/FindCommandParser.java similarity index 74% rename from src/main/java/seedu/address/logic/parser/FindCommandParser.java rename to src/main/java/seedu/budgeteer/logic/parser/FindCommandParser.java index b186a967cb94..c361816e6c8a 100644 --- a/src/main/java/seedu/address/logic/parser/FindCommandParser.java +++ b/src/main/java/seedu/budgeteer/logic/parser/FindCommandParser.java @@ -1,12 +1,13 @@ -package seedu.address.logic.parser; +package seedu.budgeteer.logic.parser; -import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.budgeteer.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 seedu.budgeteer.logic.commands.FindCommand; + +import seedu.budgeteer.logic.parser.exceptions.ParseException; +import seedu.budgeteer.model.entry.NameContainsKeywordsPredicate; /** * Parses input arguments and creates a new FindCommand object @@ -29,5 +30,4 @@ public FindCommand parse(String args) throws ParseException { return new FindCommand(new NameContainsKeywordsPredicate(Arrays.asList(nameKeywords))); } - } diff --git a/src/main/java/seedu/budgeteer/logic/parser/InvestCommandParser.java b/src/main/java/seedu/budgeteer/logic/parser/InvestCommandParser.java new file mode 100644 index 000000000000..80af96dc6f40 --- /dev/null +++ b/src/main/java/seedu/budgeteer/logic/parser/InvestCommandParser.java @@ -0,0 +1,48 @@ +package seedu.budgeteer.logic.parser; + +import static seedu.budgeteer.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.budgeteer.logic.parser.CliSyntax.PREFIX_INTEREST; +import static seedu.budgeteer.logic.parser.CliSyntax.PREFIX_YEARS; + +import java.util.stream.Stream; + +import seedu.budgeteer.logic.commands.InvestCommand; +import seedu.budgeteer.logic.parser.exceptions.ParseException; +import seedu.budgeteer.model.entry.Number; + +/** + * Parses input arguments and creates a new AddCommand object + */ +public class InvestCommandParser 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 InvestCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_INTEREST, PREFIX_YEARS); + + if (!arePrefixesPresent(argMultimap, PREFIX_INTEREST, PREFIX_YEARS) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, InvestCommand.MESSAGE_USAGE)); + } + + Number interest = ParserUtil.parseNumber(argMultimap.getValue(PREFIX_INTEREST).get()); + Number year = ParserUtil.parseNumber(argMultimap.getValue(PREFIX_YEARS).get()); + + Number temp = new Number(interest.fullNumber + " " + year.fullNumber); + + return new InvestCommand(temp); + } + + /** + * 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/budgeteer/logic/parser/LitecoinCommandParser.java b/src/main/java/seedu/budgeteer/logic/parser/LitecoinCommandParser.java new file mode 100644 index 000000000000..be8305afe045 --- /dev/null +++ b/src/main/java/seedu/budgeteer/logic/parser/LitecoinCommandParser.java @@ -0,0 +1,21 @@ +package seedu.budgeteer.logic.parser; + +import seedu.budgeteer.logic.commands.LitecoinCommand; +import seedu.budgeteer.logic.parser.exceptions.ParseException; + + +/** + * Parses input arguments and creates a new FindCommand object + */ +public class LitecoinCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the ReportCommand + * and returns an ReportCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public LitecoinCommand parse(String args) throws ParseException { + return new LitecoinCommand(); + } + + +} diff --git a/src/main/java/seedu/budgeteer/logic/parser/LockCommandParser.java b/src/main/java/seedu/budgeteer/logic/parser/LockCommandParser.java new file mode 100644 index 000000000000..555b273c8d98 --- /dev/null +++ b/src/main/java/seedu/budgeteer/logic/parser/LockCommandParser.java @@ -0,0 +1,50 @@ +package seedu.budgeteer.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.budgeteer.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.budgeteer.logic.parser.CliSyntax.PREFIX_CHANGE; +import static seedu.budgeteer.logic.parser.CliSyntax.PREFIX_REMOVE; +import static seedu.budgeteer.logic.parser.CliSyntax.PREFIX_SET; + +import java.util.stream.Stream; + +import seedu.budgeteer.logic.commands.LockCommand; +import seedu.budgeteer.logic.parser.exceptions.ParseException; + + +/** + * Parses the inputs and create a LockCommand object + */ +public class LockCommandParser implements Parser { + + + @Override + public LockCommand parse(String args) throws ParseException { + + ArgumentMultimap argumentMultimap = ArgumentTokenizer.tokenize(args, PREFIX_SET, PREFIX_CHANGE, PREFIX_REMOVE); + + if (arePrefixesPresent(argumentMultimap, PREFIX_SET)) { + return new LockCommand(new LockCommand.SetLock(argumentMultimap.getValue(PREFIX_SET).get())); + } else if (arePrefixesPresent(argumentMultimap, PREFIX_REMOVE)) { + return new LockCommand(new LockCommand.ClearLock( + argumentMultimap.getValue(PREFIX_REMOVE).get())); + } else if (arePrefixesPresent(argumentMultimap, PREFIX_CHANGE)) { + final String newPassword = argumentMultimap.getValue(PREFIX_CHANGE).get(); + requireNonNull(newPassword); + if (newPassword.length() == 0) { + throw new ParseException("Password cannot be blank!"); + } + return new LockCommand(new LockCommand.ChangeLock(newPassword)); + + } + + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, LockCommand.MESSAGE_USAGE)); + } + /** + * 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/Parser.java b/src/main/java/seedu/budgeteer/logic/parser/Parser.java similarity index 71% rename from src/main/java/seedu/address/logic/parser/Parser.java rename to src/main/java/seedu/budgeteer/logic/parser/Parser.java index d6551ad8e3ff..7540fcb86bc1 100644 --- a/src/main/java/seedu/address/logic/parser/Parser.java +++ b/src/main/java/seedu/budgeteer/logic/parser/Parser.java @@ -1,7 +1,7 @@ -package seedu.address.logic.parser; +package seedu.budgeteer.logic.parser; -import seedu.address.logic.commands.Command; -import seedu.address.logic.parser.exceptions.ParseException; +import seedu.budgeteer.logic.commands.Command; +import seedu.budgeteer.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/seedu/address/logic/parser/ParserUtil.java b/src/main/java/seedu/budgeteer/logic/parser/ParserUtil.java similarity index 53% rename from src/main/java/seedu/address/logic/parser/ParserUtil.java rename to src/main/java/seedu/budgeteer/logic/parser/ParserUtil.java index b117acb9c55b..4473b5e317ae 100644 --- a/src/main/java/seedu/address/logic/parser/ParserUtil.java +++ b/src/main/java/seedu/budgeteer/logic/parser/ParserUtil.java @@ -1,4 +1,4 @@ -package seedu.address.logic.parser; +package seedu.budgeteer.logic.parser; import static java.util.Objects.requireNonNull; @@ -6,14 +6,16 @@ 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; +import seedu.budgeteer.commons.core.Messages; +import seedu.budgeteer.commons.core.index.Index; +import seedu.budgeteer.commons.util.StringUtil; +import seedu.budgeteer.logic.parser.exceptions.ParseException; +import seedu.budgeteer.model.DirectoryPath; +import seedu.budgeteer.model.entry.CashFlow; +import seedu.budgeteer.model.entry.Date; +import seedu.budgeteer.model.entry.Name; +import seedu.budgeteer.model.entry.Number; +import seedu.budgeteer.model.tag.Tag; /** * Contains utility methods used for parsing strings in the various *Parser classes. @@ -51,48 +53,48 @@ public static Name parseName(String name) throws ParseException { } /** - * Parses a {@code String phone} into a {@code Phone}. + * Parses a {@code String name} into a {@code Name}. * Leading and trailing whitespaces will be trimmed. * - * @throws ParseException if the given {@code phone} is invalid. + * @throws ParseException if the given {@code name} 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); + public static Number parseNumber(String num) throws ParseException { + requireNonNull(num); + String trimmedName = num.trim(); + if (!Number.isValidNumber(trimmedName)) { + throw new ParseException(Number.MESSAGE_CONSTRAINTS); } - return new Phone(trimmedPhone); + return new Number(trimmedName); } /** - * Parses a {@code String address} into an {@code Address}. + * Parses a {@code String date} into a {@code Date}. * Leading and trailing whitespaces will be trimmed. * - * @throws ParseException if the given {@code address} is invalid. + * @throws ParseException if the given {@code date} 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); + public static Date parseDate(String date) throws ParseException { + requireNonNull(date); + String trimmedDate = date.trim(); + if (!Date.isValidDateFormat(trimmedDate)) { + throw new ParseException(Date.MESSAGE_DATE_CONSTRAINTS); } - return new Address(trimmedAddress); + return new Date(trimmedDate); } /** - * Parses a {@code String email} into an {@code Email}. + * Parses a {@code String cashFlow} into an {@code CashFlow}. * Leading and trailing whitespaces will be trimmed. * - * @throws ParseException if the given {@code email} is invalid. + * @throws ParseException if the given {@code CashFlow} 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); + public static CashFlow parseCashFlow(String cashFlow) throws ParseException { + requireNonNull(cashFlow); + String trimmedCashFlow = cashFlow.trim(); + if (!CashFlow.isValidCashFlow(trimmedCashFlow)) { + throw new ParseException(CashFlow.MESSAGE_CONSTRAINTS); } - return new Email(trimmedEmail); + return CashFlow.getCashFlow(trimmedCashFlow); } /** @@ -121,4 +123,15 @@ public static Set parseTags(Collection tags) throws ParseException } return tagSet; } + /** + * Parses {@code Directory Path} into a {@code DirectoryPath} + */ + public static String parseDirectoryString(String dirPath) throws ParseException { + requireNonNull(dirPath); + if (!DirectoryPath.isValidDirectory(dirPath)) { + throw new ParseException(String.format( + Messages.MESSAGE_INVALID_COMMAND_FORMAT, Messages.MESSAGE_UNREALISTIC_DIRECTORY)); + } + return dirPath; + } } diff --git a/src/main/java/seedu/address/logic/parser/Prefix.java b/src/main/java/seedu/budgeteer/logic/parser/Prefix.java similarity index 95% rename from src/main/java/seedu/address/logic/parser/Prefix.java rename to src/main/java/seedu/budgeteer/logic/parser/Prefix.java index c859d5fa5db1..de2deb0767d3 100644 --- a/src/main/java/seedu/address/logic/parser/Prefix.java +++ b/src/main/java/seedu/budgeteer/logic/parser/Prefix.java @@ -1,4 +1,4 @@ -package seedu.address.logic.parser; +package seedu.budgeteer.logic.parser; /** * A prefix that marks the beginning of an argument in an arguments string. diff --git a/src/main/java/seedu/budgeteer/logic/parser/ReportCommandParser.java b/src/main/java/seedu/budgeteer/logic/parser/ReportCommandParser.java new file mode 100644 index 000000000000..235a0534dbc9 --- /dev/null +++ b/src/main/java/seedu/budgeteer/logic/parser/ReportCommandParser.java @@ -0,0 +1,95 @@ +package seedu.budgeteer.logic.parser; + +import static seedu.budgeteer.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.budgeteer.logic.parser.CliSyntax.PREFIX_ENDDATE; +import static seedu.budgeteer.logic.parser.CliSyntax.PREFIX_INSIGHT; +import static seedu.budgeteer.logic.parser.CliSyntax.PREFIX_STARTDATE; +import static seedu.budgeteer.model.Model.PREDICATE_SHOW_ALL_ENTRYS; + +import java.util.function.Predicate; +import java.util.stream.Stream; + +import seedu.budgeteer.logic.commands.ReportCommand; +import seedu.budgeteer.logic.parser.exceptions.ParseException; +import seedu.budgeteer.model.entry.Date; +import seedu.budgeteer.model.entry.DateAfterGivenPredicate; +import seedu.budgeteer.model.entry.DateBeforeGivenPredicate; + +/** + * Parses input arguments and creates a new FindCommand object + */ +public class ReportCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the ReportCommand + * and returns an ReportCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + + private static Boolean showDetailedReport = false; + + public static Boolean isRequireDetailedReport() { + return showDetailedReport; + } + + /** + * Parses the ReportCommand with the arguments given in order to determine predicates needed to filter the list + * @param args + * @return ReportCommand object initialized with the predicates + * @throws ParseException + */ + + public ReportCommand parse(String args) throws ParseException { + Date startDate = null; + Date endDate = null; + Predicate afterStartPredicate = null; + Predicate beforeEndPredicate = null; + Predicate finalPredicate = null; + + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_STARTDATE, PREFIX_ENDDATE, + PREFIX_INSIGHT); + + if (!argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, ReportCommand.MESSAGE_USAGE)); + } + + + if (arePrefixesPresent(argMultimap, PREFIX_STARTDATE)) { + //Get Entries from this date onwards + startDate = ParserUtil.parseDate(argMultimap.getValue(PREFIX_STARTDATE).get()); + afterStartPredicate = new DateAfterGivenPredicate(startDate); + + } + + if (arePrefixesPresent(argMultimap, PREFIX_ENDDATE)) { + //Get Entries until this date + endDate = ParserUtil.parseDate(argMultimap.getValue(PREFIX_ENDDATE).get()); + beforeEndPredicate = new DateBeforeGivenPredicate(endDate); + } + + if (arePrefixesPresent(argMultimap, PREFIX_INSIGHT)) { + showDetailedReport = true; + } else { + showDetailedReport = false; + } + + if (endDate != null && startDate != null && startDate.isAfter(endDate)) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, ReportCommand.MESSAGE_USAGE)); + } else if (endDate != null && startDate != null && !startDate.isAfter(endDate)) { + finalPredicate = afterStartPredicate.and(beforeEndPredicate); + } else if (endDate == null && startDate == null) { + finalPredicate = PREDICATE_SHOW_ALL_ENTRYS; + } else { + finalPredicate = (afterStartPredicate == null) ? beforeEndPredicate : afterStartPredicate; + } + return new ReportCommand(finalPredicate); + } + + /** + * 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/SelectCommandParser.java b/src/main/java/seedu/budgeteer/logic/parser/SelectCommandParser.java similarity index 72% rename from src/main/java/seedu/address/logic/parser/SelectCommandParser.java rename to src/main/java/seedu/budgeteer/logic/parser/SelectCommandParser.java index 565b7f04bfe1..5e193cbd34b3 100644 --- a/src/main/java/seedu/address/logic/parser/SelectCommandParser.java +++ b/src/main/java/seedu/budgeteer/logic/parser/SelectCommandParser.java @@ -1,10 +1,10 @@ -package seedu.address.logic.parser; +package seedu.budgeteer.logic.parser; -import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.budgeteer.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; -import seedu.address.commons.core.index.Index; -import seedu.address.logic.commands.SelectCommand; -import seedu.address.logic.parser.exceptions.ParseException; +import seedu.budgeteer.commons.core.index.Index; +import seedu.budgeteer.logic.commands.SelectCommand; +import seedu.budgeteer.logic.parser.exceptions.ParseException; /** * Parses input arguments and creates a new SelectCommand object diff --git a/src/main/java/seedu/budgeteer/logic/parser/StockCommandParser.java b/src/main/java/seedu/budgeteer/logic/parser/StockCommandParser.java new file mode 100644 index 000000000000..27702ca72534 --- /dev/null +++ b/src/main/java/seedu/budgeteer/logic/parser/StockCommandParser.java @@ -0,0 +1,43 @@ +package seedu.budgeteer.logic.parser; + +import static seedu.budgeteer.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.budgeteer.logic.parser.CliSyntax.PREFIX_NAME; + +import java.util.stream.Stream; + +import seedu.budgeteer.logic.commands.StockCommand; +import seedu.budgeteer.logic.parser.exceptions.ParseException; +import seedu.budgeteer.model.entry.Name; + +/** + * Parses input arguments and creates a new FindCommand object + */ +public class StockCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the FindCommand + * and returns an FindCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public StockCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_NAME); + + if (!arePrefixesPresent(argMultimap, PREFIX_NAME) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, StockCommand.MESSAGE_USAGE)); + } + + Name name = ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get()); + + return new StockCommand(name); + } + + /** + * 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/exceptions/ParseException.java b/src/main/java/seedu/budgeteer/logic/parser/exceptions/ParseException.java similarity index 72% rename from src/main/java/seedu/address/logic/parser/exceptions/ParseException.java rename to src/main/java/seedu/budgeteer/logic/parser/exceptions/ParseException.java index 158a1a54c1c5..6751fbc4191f 100644 --- a/src/main/java/seedu/address/logic/parser/exceptions/ParseException.java +++ b/src/main/java/seedu/budgeteer/logic/parser/exceptions/ParseException.java @@ -1,6 +1,6 @@ -package seedu.address.logic.parser.exceptions; +package seedu.budgeteer.logic.parser.exceptions; -import seedu.address.commons.exceptions.IllegalValueException; +import seedu.budgeteer.commons.exceptions.IllegalValueException; /** * Represents a parse error encountered by a parser. diff --git a/src/main/java/seedu/budgeteer/model/DirectoryPath.java b/src/main/java/seedu/budgeteer/model/DirectoryPath.java new file mode 100755 index 000000000000..5c94725d8fe1 --- /dev/null +++ b/src/main/java/seedu/budgeteer/model/DirectoryPath.java @@ -0,0 +1,207 @@ +package seedu.budgeteer.model; + +import static java.util.Objects.requireNonNull; +import static seedu.budgeteer.commons.util.AppUtil.checkArgument; + +import java.io.File; +import java.util.logging.Logger; + +import seedu.budgeteer.commons.core.LogsCenter; +import seedu.budgeteer.commons.core.Messages; +/** + * File path is used to store the preferable location to store the Excel File when user want to export the Data + * in Excel or the user want to archive the data. + */ +public class DirectoryPath { + public static final String WORKING_DIRECTORY_STRING = System.getProperty("user.dir"); + public static final String HOME_DIRECTORY_STRING = System.getProperty("user.home"); + + public static final String DIRECTORY_REGREX = "([a-zA-Z]:)?(\\\\[a-zA-Z0-9_.-]+)+\\\\?"; + + public static final DirectoryPath HOME_DIRECTORY = new DirectoryPath(HOME_DIRECTORY_STRING); + + public static final DirectoryPath WORKING_DIRECTORY = new DirectoryPath(WORKING_DIRECTORY_STRING); + + public static final String FILE_SEPERATOR = System.getProperty("file.separator"); + + public static final String MESSAGE_DIRECTORYPATH_CONSTRAINTS = + "Path Directory should exist and only contains " + + "Alphanumeric(a-z, A-Z, 0-9), Underscore(_), Dot(.), Hyphen(-), " + + "and Backslash, only one to separate between directory.\n" + + "Please take note that inappropriate Directory format " + + "or unrealistic directory will result errors.\n" + + "If the user has not set the Directory Path for the Excel file, " + + "the Directory will be set to the User's Working Directory, named " + + WORKING_DIRECTORY_STRING + "\n" + + "For e.g., the appropriate directory is " + + HOME_DIRECTORY_STRING + + " or " + + WORKING_DIRECTORY_STRING; + + private static Logger loggerStatic = LogsCenter.getLogger(DirectoryPath.class); + + private String value; + private Logger logger = LogsCenter.getLogger(DirectoryPath.class); + + public DirectoryPath(String dirPath) { + requireNonNull(dirPath); + checkArgument((isExistingDirectory(dirPath) || isExistingFilePath(dirPath)), + String.format(Messages.MESSAGE_UNREALISTIC_DIRECTORY, MESSAGE_DIRECTORYPATH_CONSTRAINTS)); + this.value = dirPath; + } + public DirectoryPath() { + this.value = getDefaultHomeDirectoryValue(); + } + public DirectoryPath(DirectoryPath path) { + requireNonNull(path); + checkArgument(isValidDirectory(path.getDirectoryPath().getDirectoryPathValue()) + || isValidFilePath(path.getDirectoryPath().getDirectoryPathValue())); + this.value = path.getDirectoryPath().getDirectoryPathValue(); + } + + /** + * Remove all the whitespace in the directory path. + * @param dirPath the given directory path. + * @return String with no whitespace. + */ + public static String removeWhiteSpace(String dirPath) { + requireNonNull(dirPath); + StringBuilder stringBuilder = new StringBuilder(); + String[] words = dirPath.split("\\s+"); + + for (String word : words) { + stringBuilder.append(word.trim()); + } + loggerStatic.info("The String without the whitespace" + stringBuilder.toString()); + return stringBuilder.toString(); + } + + /** + * Check if the Directory Path is set by checking if the path == null or not. + * @return boolean value. + */ + public boolean isDirectorySet() { + return !(value == null); + } + + /** + * Update/set the directory path for the excel file. + * @param directoryPath the String directory path for the excel file. + */ + public void updateDirectoryPath(String directoryPath) { + requireNonNull(directoryPath); + value = directoryPath; + logger.info("The new directory is " + directoryPath); + } + + /** + * Update/set the directory path for the excel file. + * @param path the Directory Path for the excel file. + */ + public void updateDirectoryPath(DirectoryPath path) { + requireNonNull(path); + value = path.getDirectoryPathValue(); + logger.info("The new directory is " + path); + } + /** + * Change the user optional Directory path to null. + * Therefore, when we want to + */ + public String removeDirectoryPath() { + value = null; + return String.format("The User's choose Directory path has been removed, " + + "hence, we will use the Default User's Working Directory, named %1$s", + getDefaultWorkingDirectoryValue()); + } + + /** + * Check if the Directory Path is valid or not by checking Valid format and Existing path in the User's computer. + * @param dirPath the given Directory path. + * @return boolean value. + */ + public static Boolean isValidDirectory(String dirPath) { + return isExistingDirectory(dirPath); + } + + /** + * Check if the Directory Path is valid or not by checking Valid format and Existing path in the User's computer. + * @param filePath the given Directory path. + * @return boolean value. + */ + public static Boolean isValidFilePath(String filePath) { + return isExistingFilePath(filePath); + } + + /** + * Check whether the Directory follows the format. + * @param dirPath the given Directory path. + * @return boolean value. + */ + public static Boolean isValidFormat(String dirPath) { + return removeWhiteSpace(dirPath).matches(DIRECTORY_REGREX); + } + + /** + * Check whether the given directory is realistic or not. + * @return boolean value to indicate whether the directory exists. + */ + public static Boolean isExistingDirectory(String dirPath) { + File file = new File(dirPath); + return file.isDirectory(); + } + + /** + * Check whether the given directory is realistic or not. + * @return boolean value to indicate whether the directory exists. + */ + public static Boolean isExistingFilePath(String dirPath) { + File file = new File(dirPath); + return file.isFile(); + } + + /** + * Check whether the given directory is realistic or not. + * @param filePath the given file path is realistic or not. + * @return boolean value to indicate whether the directory exists. + */ + public static Boolean isExistingFile(String filePath) { + File file = new File(filePath); + return file.isFile(); + } + + public String getValue() { + return value; + } + + public DirectoryPath getDirectoryPath() { + return (value == null) ? WORKING_DIRECTORY : new DirectoryPath(value); + } + + public String getDirectoryPathValue() { + return (value == null) ? WORKING_DIRECTORY_STRING : value; + } + + public static String getDefaultWorkingDirectoryValue() { + return WORKING_DIRECTORY_STRING; + } + + public static String getDefaultHomeDirectoryValue() { + return HOME_DIRECTORY_STRING; + } + + public static DirectoryPath getDefaultWorkingDirectory() { + return WORKING_DIRECTORY; + } + + public static DirectoryPath getDefaultHomeDirectory() { + return HOME_DIRECTORY; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof DirectoryPath // instanceof handles nulls + && (this.getDirectoryPath().getDirectoryPathValue()) + .equals(((DirectoryPath) other).getDirectoryPath().getDirectoryPathValue())); + } +} diff --git a/src/main/java/seedu/budgeteer/model/EntriesBook.java b/src/main/java/seedu/budgeteer/model/EntriesBook.java new file mode 100644 index 000000000000..29e6eb17df14 --- /dev/null +++ b/src/main/java/seedu/budgeteer/model/EntriesBook.java @@ -0,0 +1,159 @@ +package seedu.budgeteer.model; + +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import javafx.beans.InvalidationListener; +import javafx.collections.ObservableList; +import seedu.budgeteer.commons.util.InvalidationListenerManager; +import seedu.budgeteer.model.entry.Entry; +import seedu.budgeteer.model.entry.EntryList; + +/** + * Wraps all data at the budgeteer-book level + * Duplicates are not allowed (by .isSameEntry comparison) + */ +public class EntriesBook implements ReadOnlyEntriesBook { + + private final EntryList entrys; + private final InvalidationListenerManager invalidationListenerManager = new InvalidationListenerManager(); + + /* + * The 'unusual' code block below is an 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. + */ + { + entrys = new EntryList(); + } + + public EntriesBook() {} + + /** + * Creates an EntriesBook using the Entrys in the {@code toBeCopied} + */ + public EntriesBook(ReadOnlyEntriesBook toBeCopied) { + this(); + resetData(toBeCopied); + } + + //// list overwrite operations + + /** + * Replaces the contents of the entry list with {@code entrys}. + * {@code entrys} must not contain duplicate entrys. + */ + public void setEntrys(List entrys) { + this.entrys.setEntrys(entrys); + indicateModified(); + } + + /** + * Resets the existing data of this {@code EntriesBook} with {@code newData}. + */ + public void resetData(ReadOnlyEntriesBook newData) { + requireNonNull(newData); + + setEntrys(newData.getEntryList()); + } + + //// entry-level operations + + /** + * Returns true if a entry with the same identity as {@code entry} exists in the budgeteer book. + */ + public boolean hasEntry(Entry entry) { + requireNonNull(entry); + return entrys.contains(entry); + } + + /** + * Adds a entry to the budgeteer book. + * The entry must not already exist in the budgeteer book. + */ + public void addEntry(Entry p) { + entrys.add(p); + indicateModified(); + } + + /** + * returns true if there are two limits share the same dates. + * @param limitin + * @return + */ + + + /** + * Replaces the given entry {@code target} in the list with {@code editedEntry}. + * {@code target} must exist in the budgeteer book. + * The entry identity of {@code editedEntry} must not be the same as another existing entry in the budgeteer book. + */ + public void setEntry(Entry target, Entry editedEntry) { + requireNonNull(editedEntry); + + entrys.setEntry(target, editedEntry); + indicateModified(); + } + + /** + * Removes {@code key} from this {@code EntriesBook}. + * {@code key} must exist in the budgeteer book. + */ + public void removeEntry(Entry key) { + entrys.remove(key); + indicateModified(); + } + + + @Override + public void addListener(InvalidationListener listener) { + invalidationListenerManager.addListener(listener); + } + + @Override + public void removeListener(InvalidationListener listener) { + invalidationListenerManager.removeListener(listener); + } + + /** + * Notifies listeners that the budgeteer book has been modified. + */ + protected void indicateModified() { + invalidationListenerManager.callListeners(this); + } + + //// util methods + + @Override + public String toString() { + return entrys.asUnmodifiableObservableList().size() + " entrys"; + } + + @Override + public ObservableList getEntryList() { + return entrys.asUnmodifiableObservableList(); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof EntriesBook // instanceof handles nulls + && entrys.equals(((EntriesBook) other).entrys)); + } + + @Override + public int hashCode() { + return entrys.hashCode(); + } + + /** + * Displays sorted data records in EntriesBook + */ + public void sortEntrys(String category, Boolean ascending) { + entrys.sortEntrys(category, ascending); + } + +} diff --git a/src/main/java/seedu/budgeteer/model/Model.java b/src/main/java/seedu/budgeteer/model/Model.java new file mode 100644 index 000000000000..d8d969d33784 --- /dev/null +++ b/src/main/java/seedu/budgeteer/model/Model.java @@ -0,0 +1,142 @@ +package seedu.budgeteer.model; + +import java.nio.file.Path; +import java.util.function.Predicate; + +import javafx.beans.property.ReadOnlyProperty; +import javafx.collections.ObservableList; +import seedu.budgeteer.commons.core.GuiSettings; +import seedu.budgeteer.model.entry.Entry; + +/** + * The API of the Model component. + */ +public interface Model { + /** {@code Predicate} that always evaluate to true */ + Predicate PREDICATE_SHOW_ALL_ENTRYS = unused -> true; + /** {@code Predicate} that always evaluate to false */ + Predicate PREDICATE_HIDE_ALL_ENTRYS = unused -> false; + + + + /** + * 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' budgeteer book file path. + */ + Path getAddressBookFilePath(); + + /** + * Sets the user prefs' budgeteer book file path. + */ + void setAddressBookFilePath(Path addressBookFilePath); + + /** + * Replaces budgeteer book data with the data in {@code addressBook}. + */ + void setAddressBook(ReadOnlyEntriesBook addressBook); + + /** Returns the EntriesBook */ + ReadOnlyEntriesBook getAddressBook(); + + /** + * Returns true if a entry with the same identity as {@code entry} exists in the budgeteer book. + */ + boolean hasEntry(Entry entry); + + /** + * Deletes the given entry. + * The entry must exist in the budgeteer book. + */ + void deleteEntry(Entry target); + + /** + * Adds the given entry. + * {@code entry} must not already exist in the budgeteer book. + */ + void addEntry(Entry entry); + + /** + * Replaces the given entry {@code target} with {@code editedEntry}. + * {@code target} must exist in the budgeteer book. + * The entry identity of {@code editedEntry} must not be the same as another existing entry in the budgeteer book. + */ + void setEntry(Entry target, Entry editedEntry); + + /** Returns an unmodifiable view of the filtered entry list */ + ObservableList getFilteredEntryList(); + + /** + * Updates the filter of the filtered entry list to filter by the given {@code predicate}. + * @throws NullPointerException if {@code predicate} is null. + */ + void updateFilteredEntryList(Predicate predicate); + + /** + * Returns true if the model has previous budgeteer book states to restore. + */ + boolean canUndoAddressBook(); + + /** + * Returns true if the model has undone budgeteer book states to restore. + */ + boolean canRedoAddressBook(); + + /** + * Restores the model's budgeteer book to its previous state. + */ + void undoAddressBook(); + + /** + * Restores the model's budgeteer book to its previously undone state. + */ + void redoAddressBook(); + + /** + * Saves the current budgeteer book state for undo/redo. + */ + void commitAddressBook(); + + + /** + * Updates the filtered record list to sort by the given {@code category}. + * @throws NullPointerException if {@code predicate} is null. + */ + void sortFilteredEntryList(String category, Boolean reversed); + + /** + * Selected entry in the filtered entry list. + * null if no entry is selected. + */ + ReadOnlyProperty selectedEntryProperty(); + + /** + * Returns the selected entry in the filtered entry list. + * null if no entry is selected. + */ + Entry getSelectedEntry(); + + /** + * Sets the selected entry in the filtered entry list. + */ + void setSelectedEntry(Entry entry); + +} diff --git a/src/main/java/seedu/budgeteer/model/ModelManager.java b/src/main/java/seedu/budgeteer/model/ModelManager.java new file mode 100644 index 000000000000..db286bdbd5f7 --- /dev/null +++ b/src/main/java/seedu/budgeteer/model/ModelManager.java @@ -0,0 +1,254 @@ +package seedu.budgeteer.model; + +import static java.util.Objects.requireNonNull; +import static seedu.budgeteer.commons.util.CollectionUtil.requireAllNonNull; + +import java.nio.file.Path; +import java.util.Objects; +import java.util.function.Predicate; +import java.util.logging.Logger; + +import javafx.beans.property.ReadOnlyProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; +import javafx.collections.transformation.FilteredList; +import seedu.budgeteer.commons.core.GuiSettings; +import seedu.budgeteer.commons.core.LogsCenter; +import seedu.budgeteer.model.entry.Date; +import seedu.budgeteer.model.entry.Entry; +import seedu.budgeteer.model.entry.exceptions.EntryNotFoundException; + +/** + * Represents the in-memory model of the budgeteer book data. + */ +public class ModelManager implements Model { + private static final Logger logger = LogsCenter.getLogger(ModelManager.class); + private static final Date DATE_SPECIAL_FOR_MONTHLY = new Date("01-01-9999"); + private final VersionedEntriesBook versionedAddressBook; + private final UserPrefs userPrefs; + private final FilteredList filteredEntrys; + private final SimpleObjectProperty selectedEntry = new SimpleObjectProperty<>(); + + /** + * Initializes a ModelManager with the given addressBook and userPrefs. + */ + public ModelManager(ReadOnlyEntriesBook addressBook, ReadOnlyUserPrefs userPrefs) { + super(); + requireAllNonNull(addressBook, userPrefs); + + logger.fine("Initializing with budgeteer book: " + addressBook + " and user prefs " + userPrefs); + + versionedAddressBook = new VersionedEntriesBook(addressBook); + this.userPrefs = new UserPrefs(userPrefs); + filteredEntrys = new FilteredList<>(versionedAddressBook.getEntryList()); + filteredEntrys.addListener(this::ensureSelectedEntryIsValid); + } + + public ModelManager() { + this(new EntriesBook(), new UserPrefs()); + } + + /** + * removes tag from all entrys + * + * @return + */ + + + //=========== 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); + } + + //=========== EntriesBook ================================================================================ + + @Override + public void setAddressBook(ReadOnlyEntriesBook addressBook) { + versionedAddressBook.resetData(addressBook); + } + + @Override + public ReadOnlyEntriesBook getAddressBook() { + return versionedAddressBook; + } + + @Override + public boolean hasEntry(Entry entry) { + requireNonNull(entry); + return versionedAddressBook.hasEntry(entry); + } + + @Override + public void deleteEntry(Entry target) { + versionedAddressBook.removeEntry(target); + } + + @Override + public void addEntry(Entry entry) { + versionedAddressBook.addEntry(entry); + updateFilteredEntryList(PREDICATE_SHOW_ALL_ENTRYS); + } + + @Override + public void setEntry(Entry target, Entry editedEntry) { + requireAllNonNull(target, editedEntry); + + versionedAddressBook.setEntry(target, editedEntry); + } + + + + //=========== Filtered Entry List Accessors ============================================================= + + /** + * Returns an unmodifiable view of the list of {@code Entry} backed by the internal list of + * {@code versionedAddressBook} + */ + @Override + public ObservableList getFilteredEntryList() { + return filteredEntrys; + } + + @Override + public void updateFilteredEntryList(Predicate predicate) { + requireNonNull(predicate); + filteredEntrys.setPredicate(predicate); + } + + //=========== Undo/Redo ================================================================================= + + @Override + public boolean canUndoAddressBook() { + return versionedAddressBook.canUndo(); + } + + @Override + public boolean canRedoAddressBook() { + return versionedAddressBook.canRedo(); + } + + @Override + public void undoAddressBook() { + versionedAddressBook.undo(); + } + + @Override + public void redoAddressBook() { + versionedAddressBook.redo(); + } + + @Override + public void commitAddressBook() { + versionedAddressBook.commit(); + } + + //=========== Modifying Entry List Accessors ============================================================= + + @Override + public void sortFilteredEntryList(String category, Boolean ascending) { + requireAllNonNull(category, ascending); + versionedAddressBook.sortEntrys(category, ascending); + } + + + //=========== Selected entry =========================================================================== + + @Override + public ReadOnlyProperty selectedEntryProperty() { + return selectedEntry; + } + + @Override + public Entry getSelectedEntry() { + return selectedEntry.getValue(); + } + + @Override + public void setSelectedEntry(Entry entry) { + if (entry != null && !filteredEntrys.contains(entry)) { + throw new EntryNotFoundException(); + } + selectedEntry.setValue(entry); + } + + /** + * Ensures {@code selectedEntry} is a valid entry in {@code filteredEntrys}. + */ + private void ensureSelectedEntryIsValid(ListChangeListener.Change change) { + while (change.next()) { + if (selectedEntry.getValue() == null) { + // null is always a valid selected entry, so we do not need to check that it is valid anymore. + return; + } + + boolean wasSelectedEntryReplaced = change.wasReplaced() && change.getAddedSize() == change.getRemovedSize() + && change.getRemoved().contains(selectedEntry.getValue()); + if (wasSelectedEntryReplaced) { + // Update selectedEntry to its new value. + int index = change.getRemoved().indexOf(selectedEntry.getValue()); + selectedEntry.setValue(change.getAddedSubList().get(index)); + continue; + } + + boolean wasSelectedEntryRemoved = change.getRemoved().stream() + .anyMatch(removedEntry -> selectedEntry.getValue().isSameEntry(removedEntry)); + if (wasSelectedEntryRemoved) { + // Select the entry that came before it in the list, + // or clear the selection if there is no such entry. + selectedEntry.setValue(change.getFrom() > 0 ? change.getList().get(change.getFrom() - 1) : null); + } + } + } + + @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 versionedAddressBook.equals(other.versionedAddressBook) + && userPrefs.equals(other.userPrefs) + && filteredEntrys.equals(other.filteredEntrys) + && Objects.equals(selectedEntry.get(), other.selectedEntry.get()); + } + +} + diff --git a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java b/src/main/java/seedu/budgeteer/model/ReadOnlyEntriesBook.java similarity index 50% rename from src/main/java/seedu/address/model/ReadOnlyAddressBook.java rename to src/main/java/seedu/budgeteer/model/ReadOnlyEntriesBook.java index 6a301434b33b..05e333788f0d 100644 --- a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java +++ b/src/main/java/seedu/budgeteer/model/ReadOnlyEntriesBook.java @@ -1,18 +1,19 @@ -package seedu.address.model; +package seedu.budgeteer.model; import javafx.beans.Observable; import javafx.collections.ObservableList; -import seedu.address.model.person.Person; +import seedu.budgeteer.model.entry.Entry; + /** - * Unmodifiable view of an address book + * Unmodifiable view of an budgeteer book */ -public interface ReadOnlyAddressBook extends Observable { +public interface ReadOnlyEntriesBook extends Observable { /** * Returns an unmodifiable view of the persons list. * This list will not contain any duplicate persons. */ - ObservableList getPersonList(); + ObservableList getEntryList(); } diff --git a/src/main/java/seedu/address/model/ReadOnlyUserPrefs.java b/src/main/java/seedu/budgeteer/model/ReadOnlyUserPrefs.java similarity index 61% rename from src/main/java/seedu/address/model/ReadOnlyUserPrefs.java rename to src/main/java/seedu/budgeteer/model/ReadOnlyUserPrefs.java index befd58a4c739..ce1e4b4045b0 100644 --- a/src/main/java/seedu/address/model/ReadOnlyUserPrefs.java +++ b/src/main/java/seedu/budgeteer/model/ReadOnlyUserPrefs.java @@ -1,8 +1,8 @@ -package seedu.address.model; +package seedu.budgeteer.model; import java.nio.file.Path; -import seedu.address.commons.core.GuiSettings; +import seedu.budgeteer.commons.core.GuiSettings; /** * Unmodifiable view of user prefs. @@ -13,4 +13,5 @@ public interface ReadOnlyUserPrefs { Path getAddressBookFilePath(); + String getPasswordFilePath(); } diff --git a/src/main/java/seedu/address/model/UserPrefs.java b/src/main/java/seedu/budgeteer/model/UserPrefs.java similarity index 86% rename from src/main/java/seedu/address/model/UserPrefs.java rename to src/main/java/seedu/budgeteer/model/UserPrefs.java index 25a5fd6eab9e..8d24375aff27 100644 --- a/src/main/java/seedu/address/model/UserPrefs.java +++ b/src/main/java/seedu/budgeteer/model/UserPrefs.java @@ -1,4 +1,4 @@ -package seedu.address.model; +package seedu.budgeteer.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 seedu.budgeteer.commons.core.GuiSettings; /** * Represents User's preferences. @@ -15,6 +15,7 @@ public class UserPrefs implements ReadOnlyUserPrefs { private GuiSettings guiSettings = new GuiSettings(); private Path addressBookFilePath = Paths.get("data" , "addressbook.json"); + private String passwordFilePath = "data/password.txt"; /** * Creates a {@code UserPrefs} with default values. @@ -84,4 +85,11 @@ public String toString() { return sb.toString(); } + public String getPasswordFilePath() { + return passwordFilePath; + } + + public void setPasswordFilePath(String passwordFilePath) { + this.passwordFilePath = passwordFilePath; + } } diff --git a/src/main/java/seedu/address/model/VersionedAddressBook.java b/src/main/java/seedu/budgeteer/model/VersionedEntriesBook.java similarity index 73% rename from src/main/java/seedu/address/model/VersionedAddressBook.java rename to src/main/java/seedu/budgeteer/model/VersionedEntriesBook.java index e17a9e3ba4ab..fc5925843dea 100644 --- a/src/main/java/seedu/address/model/VersionedAddressBook.java +++ b/src/main/java/seedu/budgeteer/model/VersionedEntriesBook.java @@ -1,31 +1,31 @@ -package seedu.address.model; +package seedu.budgeteer.model; import java.util.ArrayList; import java.util.List; /** - * {@code AddressBook} that keeps track of its own history. + * {@code EntriesBook} that keeps track of its own history. */ -public class VersionedAddressBook extends AddressBook { +public class VersionedEntriesBook extends EntriesBook { - private final List addressBookStateList; + private final List addressBookStateList; private int currentStatePointer; - public VersionedAddressBook(ReadOnlyAddressBook initialState) { + public VersionedEntriesBook(ReadOnlyEntriesBook initialState) { super(initialState); addressBookStateList = new ArrayList<>(); - addressBookStateList.add(new AddressBook(initialState)); + addressBookStateList.add(new EntriesBook(initialState)); currentStatePointer = 0; } /** - * Saves a copy of the current {@code AddressBook} state at the end of the state list. + * Saves a copy of the current {@code EntriesBook} state at the end of the state list. * Undone states are removed from the state list. */ public void commit() { removeStatesAfterCurrentPointer(); - addressBookStateList.add(new AddressBook(this)); + addressBookStateList.add(new EntriesBook(this)); currentStatePointer++; indicateModified(); } @@ -35,7 +35,7 @@ private void removeStatesAfterCurrentPointer() { } /** - * Restores the address book to its previous state. + * Restores the budgeteer book to its previous state. */ public void undo() { if (!canUndo()) { @@ -46,7 +46,7 @@ public void undo() { } /** - * Restores the address book to its previously undone state. + * Restores the budgeteer book to its previously undone state. */ public void redo() { if (!canRedo()) { @@ -57,14 +57,14 @@ public void redo() { } /** - * Returns true if {@code undo()} has address book states to undo. + * Returns true if {@code undo()} has budgeteer book states to undo. */ public boolean canUndo() { return currentStatePointer > 0; } /** - * Returns true if {@code redo()} has address book states to redo. + * Returns true if {@code redo()} has budgeteer book states to redo. */ public boolean canRedo() { return currentStatePointer < addressBookStateList.size() - 1; @@ -78,11 +78,11 @@ public boolean equals(Object other) { } // instanceof handles nulls - if (!(other instanceof VersionedAddressBook)) { + if (!(other instanceof VersionedEntriesBook)) { return false; } - VersionedAddressBook otherVersionedAddressBook = (VersionedAddressBook) other; + VersionedEntriesBook otherVersionedAddressBook = (VersionedEntriesBook) other; // state check return super.equals(otherVersionedAddressBook) diff --git a/src/main/java/seedu/budgeteer/model/entry/CashFlow.java b/src/main/java/seedu/budgeteer/model/entry/CashFlow.java new file mode 100644 index 000000000000..ac8bb3650c3b --- /dev/null +++ b/src/main/java/seedu/budgeteer/model/entry/CashFlow.java @@ -0,0 +1,123 @@ +package seedu.budgeteer.model.entry; + +import static java.lang.Double.isFinite; +import static java.util.Objects.requireNonNull; +import static seedu.budgeteer.commons.util.AppUtil.checkArgument; + +/** + * Represents any form of cash flow in a record in the book + * Guarantees: immutable; is valid as declared in {@link #isValidCashFlow(String)} + */ +public class CashFlow { + public static final String MESSAGE_CONSTRAINTS = + "Any form of cash flow should consist of '+' or '-' (Optional), " + + "followed by a sequence of characters consisting of only digits and/or decimal points ('.')." + + "It must be of the following form .:\n" + + "1. cannot start from '0' unless it has only 1 digit. " + + "There must be at least 1 digit in this field.\n" + + "2. At most 1 decimal point can be present. Decimal point is optional." + + "If decimal point is present, it must have at least 1 digit and at most 2 digits after it.\n" + + "3. The maximum whole number allowed is 1e12 - 1. Anything more than this is not allowed. "; + + public static final String SIGN_REGEX = ("(?[-+])"); + public static final String CASHFLOW_NO_SIGN_REGEX = ("(?.*)"); + public static final String CURRENCY = "$"; + public static final String POSITIVE_SIGN = "+"; + public static final String NEGATIVE_SIGN = "-"; + public static final String REPRESENTATION_ZERO = "-0"; + public static final String FORMAT_STANDARD_CASH = "%.2f"; + public static final String FORMAT_STANDARD_MONEY = "%.2f"; + public static final Double MAX_CASH = 99999999999.99; + + private static final String CASHFLOW_VALIDATION_REGEX = "^[+-]?\\$?(0|[1-9]\\d{0,11})(\\.\\d{1,2})?"; + + public final String value; + public final Double valueDouble; + + public CashFlow(String cashFlow) { + requireNonNull(cashFlow); + checkArgument(isValidMoneyFlow(cashFlow), MESSAGE_CONSTRAINTS); + cashFlow = cashFlow.replace("$", ""); + this.valueDouble = Double.valueOf(cashFlow); + this.value = generateString(); + checkArgument(isFinite(valueDouble), MESSAGE_CONSTRAINTS); + } + + private CashFlow(Double cashFlow) { + requireNonNull(cashFlow); + checkArgument(isValidMoneyFlow(cashFlow.toString()), MESSAGE_CONSTRAINTS); + this.valueDouble = cashFlow; + this.value = generateString(); + checkArgument(isFinite(valueDouble), MESSAGE_CONSTRAINTS); + } + + public static boolean isValidMoneyFlow(String test) { + return test.matches(CASHFLOW_VALIDATION_REGEX); + } + + /** + * Static method to get an instance of CashFlow using the class's private constructors + * Checks whether the supplied argument cashFlow is of type String or Double and calls the correct constructor + * for it. + * + * If the supplied argument is not an instance of String or Double, Throws an IllegalArgumentException. + * @param cashFlow (Float/String) -- The value of the CashFlow + * @return the generated CashFlow Instance + */ + public static CashFlow getCashFlow (Object cashFlow) { + CashFlow cashFlowInstance; + + if (cashFlow instanceof String) { + String cashFlowStr = (String) cashFlow; + cashFlowInstance = new CashFlow(cashFlowStr); + return cashFlowInstance; + } else if (cashFlow instanceof Double) { + Double cashFlowDbl = (Double) cashFlow; + cashFlowInstance = new CashFlow(cashFlowDbl); + return cashFlowInstance; + } + + requireNonNull(cashFlow); + throw new IllegalArgumentException("CashFlow requires a double/ string argument for its constructor"); + } + /** + * Returns if a given string is a valid cashflow parameter. + */ + public static boolean isValidCashFlow(String test) { + return test.matches(CASHFLOW_VALIDATION_REGEX); + } + + /** + * Returns a stylised string for display + * + */ + private String generateString() { + if (String.format(FORMAT_STANDARD_CASH, Math.abs(valueDouble)).equals("0.00")) { + return CURRENCY + String.format(FORMAT_STANDARD_CASH, Math.abs(valueDouble)); + } else if (valueDouble > 0) { + return POSITIVE_SIGN + CURRENCY + String.format(FORMAT_STANDARD_CASH, Math.abs(valueDouble)); + } else { + return NEGATIVE_SIGN + CURRENCY + String.format(FORMAT_STANDARD_CASH, Math.abs(valueDouble)); + } + + } + + public String toString() { + return generateString(); + } + public double toDouble () { + return valueDouble; + } + + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof CashFlow // instanceof handles nulls + && value.equals(((CashFlow) other).value)); // state check + } + + public int hashCode() { + return value.hashCode(); + } +} diff --git a/src/main/java/seedu/budgeteer/model/entry/CashFlowContainsSpecifiedKeywordsPredicate.java b/src/main/java/seedu/budgeteer/model/entry/CashFlowContainsSpecifiedKeywordsPredicate.java new file mode 100644 index 000000000000..e88d71e3e745 --- /dev/null +++ b/src/main/java/seedu/budgeteer/model/entry/CashFlowContainsSpecifiedKeywordsPredicate.java @@ -0,0 +1,31 @@ +package seedu.budgeteer.model.entry; + +import java.util.List; +import java.util.function.Predicate; + +import seedu.budgeteer.commons.util.StringUtil; + +/** + * Tests that a {@code Entry}'s {@code Cashflow} matches any of the keywords given. + */ +public class CashFlowContainsSpecifiedKeywordsPredicate implements Predicate { + private final List keywords; + + public CashFlowContainsSpecifiedKeywordsPredicate(List keywords) { + this.keywords = keywords; + } + + @Override + public boolean test(Entry entry) { + return keywords.stream() + .anyMatch(keyword -> StringUtil.containsWordIgnoreCase(entry.getCashFlow().value, keyword)); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof CashFlowContainsSpecifiedKeywordsPredicate // instanceof handles nulls + && this.keywords.equals(((CashFlowContainsSpecifiedKeywordsPredicate) other).keywords)); // state check + } + +} diff --git a/src/main/java/seedu/budgeteer/model/entry/Date.java b/src/main/java/seedu/budgeteer/model/entry/Date.java new file mode 100644 index 000000000000..d1196b43a193 --- /dev/null +++ b/src/main/java/seedu/budgeteer/model/entry/Date.java @@ -0,0 +1,169 @@ +package seedu.budgeteer.model.entry; + +import static java.util.Objects.requireNonNull; +import static seedu.budgeteer.commons.util.AppUtil.checkArgument; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +import seedu.budgeteer.commons.util.DateUtil; + +/** + * Represents a Record's date in the financial budgeteer. + * Guarantees: immutable; is valid as declared in {@link #isValidDateFormat(String)} + */ +public class Date { + public static final String MESSAGE_DATE_CONSTRAINTS = + "Date parameter should be in the format of dd-mm-yyyy " + + "with dd and mm being 1 or 2 digits, and yyyy being 4 digits.\n" + + "Please take note that inappropriate date will result in errors, for example: 30/02/2019"; + public static final String MESSAGE_DATE_LOGICAL_CONSTRAINTS = + "Date should follow the modern calendar. " + + "Day parameter must fit within the constraints of each month. \n" + + "For e.g, February has only 28 days for the non-Leap year " + + "so the day parameter must be less than or equal to 28 if the month " + + "parameter is 2."; + public static final String DATE_VALIDATION_REGEX = "[0-3]?\\d{1}-[0-1]?\\d{1}-\\d{4}"; + + public static final String DATE_INPUT_TODAY = "today"; + public static final String DATE_INPUT_YESTERDAY = "ytd"; + + private static DateTimeFormatter dtFormat = DateTimeFormatter.ofPattern("dd-MM-yyyy"); + + public final String value; + + private int day; + private int month; + private int year; + + private LocalDate localDate; + + /** + * Constructs a {@code Date}. + * @param date A valid date. + */ + public Date(String date) { + requireNonNull(date); + // Get Representation for today and yesterday + if (date.equalsIgnoreCase(DATE_INPUT_TODAY)) { + LocalDate todayDate = LocalDate.now(); + date = todayDate.format(dtFormat); + } else if (date.equalsIgnoreCase(DATE_INPUT_YESTERDAY)) { + LocalDate ytdDate = LocalDate.now().minusDays(1L); + date = ytdDate.format(dtFormat); + } + + checkArgument(isValidDateFormat(date), MESSAGE_DATE_CONSTRAINTS); + splitDate(date); + value = getStandardValue(); + checkArgument(DateUtil.isValidDate(day, month, year), MESSAGE_DATE_LOGICAL_CONSTRAINTS); + localDate = LocalDate.of(year, month, day); + } + + + /** + * Change the (String)value to some Standard Value (follow the format dd-mm-yyyy) + * @return standard value Date + */ + public String getStandardValue() { + String standardDay; + String standardMonth; + String standardYear; + if (day > 0 && day < 10 && String.valueOf(day).length() == 1) { + standardDay = "0" + String.valueOf(day); + } else { + standardDay = String.valueOf(day); + } + if (month > 0 && month < 10 && String.valueOf(month).length() == 1) { + standardMonth = "0" + String.valueOf(month); + } else { + standardMonth = String.valueOf(month); + } + standardYear = String.valueOf(year); + String standardValue = + String.format(standardDay + "-" + standardMonth + "-" + standardYear); + return standardValue; + } + + /** + * Transform the Date into standard Date following the format dd-mm-yyyy + * @return standard Date + */ + public Date getStandardDate() { + return new Date (getStandardValue()); + } + /** + * Splits a date into the different parameters and assigns them to day,month,year + * Format specified: dd-mm-yyyy + * @param date + */ + private void splitDate(String date) { + String[] dateParams = date.split("-"); + day = Integer.parseInt(dateParams[0]); + month = Integer.parseInt(dateParams[1]); + year = Integer.parseInt(dateParams[2]); + + } + + /** + * Returns true if the string to be tested is a valid date format, false otherwise + * @param test + */ + public static boolean isValidDateFormat(String test) { + return (test.matches(DATE_VALIDATION_REGEX) || test.equalsIgnoreCase(DATE_INPUT_YESTERDAY) + || test.equalsIgnoreCase(DATE_INPUT_TODAY)); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + public String getValue() { + return value; + } + + public int getDay() { + return day; + } + + public int getMonth() { + return month; + } + + public int getYear() { + return year; + } + + public LocalDate getLocalDate() { + return localDate; + } + + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Date // instanceof handles nulls + && day == ((Date) other).getDay() + && month == ((Date) other).getMonth() + && year == ((Date) other).getYear()) + || (other instanceof Date) + && this.localDate.equals(((Date) other).getLocalDate()); // state check + } + + public boolean isBefore(Date other) { + return (this.localDate.isBefore(other.getLocalDate())); + } + + public boolean isAfter(Date other) { + return (this.localDate.isAfter(other.getLocalDate())); + } + + public boolean isBetween(Date startDate, Date endDate) { + return (this.localDate.isAfter(startDate.getLocalDate()) && this.localDate.isBefore(endDate.getLocalDate())); + } +} diff --git a/src/main/java/seedu/budgeteer/model/entry/DateAfterGivenPredicate.java b/src/main/java/seedu/budgeteer/model/entry/DateAfterGivenPredicate.java new file mode 100644 index 000000000000..28484e16a21f --- /dev/null +++ b/src/main/java/seedu/budgeteer/model/entry/DateAfterGivenPredicate.java @@ -0,0 +1,28 @@ +package seedu.budgeteer.model.entry; + +import java.util.function.Predicate; + +/** + * Tests that a {@code Entry}'s {@code Name} matches any of the keywords given. + */ +public class DateAfterGivenPredicate implements Predicate { + private final Date specifiedDate; + + public DateAfterGivenPredicate(Date specifiedDate) { + this.specifiedDate = specifiedDate; + } + + @Override + public boolean test(Entry entry) { + Date entryDate = entry.getDate(); + return entryDate.isAfter(specifiedDate) || entryDate.equals(specifiedDate); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof DateAfterGivenPredicate // instanceof handles nulls + && specifiedDate.equals(((DateAfterGivenPredicate) other).specifiedDate)); // state check + } + +} diff --git a/src/main/java/seedu/budgeteer/model/entry/DateBeforeGivenPredicate.java b/src/main/java/seedu/budgeteer/model/entry/DateBeforeGivenPredicate.java new file mode 100644 index 000000000000..dbf544fb0662 --- /dev/null +++ b/src/main/java/seedu/budgeteer/model/entry/DateBeforeGivenPredicate.java @@ -0,0 +1,28 @@ +package seedu.budgeteer.model.entry; + +import java.util.function.Predicate; + +/** + * Tests that a {@code Entry}'s {@code Name} matches any of the keywords given. + */ +public class DateBeforeGivenPredicate implements Predicate { + private final Date specifiedDate; + + public DateBeforeGivenPredicate(Date specifiedDate) { + this.specifiedDate = specifiedDate; + } + + @Override + public boolean test(Entry entry) { + Date entryDate = entry.getDate(); + return entryDate.isBefore(specifiedDate) || entryDate.equals(specifiedDate); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof DateBeforeGivenPredicate // instanceof handles nulls + && specifiedDate.equals(((DateBeforeGivenPredicate) other).specifiedDate)); // state check + } + +} diff --git a/src/main/java/seedu/budgeteer/model/entry/DateContainsSpecifiedKeywordsPredicate.java b/src/main/java/seedu/budgeteer/model/entry/DateContainsSpecifiedKeywordsPredicate.java new file mode 100644 index 000000000000..8bf9234cf392 --- /dev/null +++ b/src/main/java/seedu/budgeteer/model/entry/DateContainsSpecifiedKeywordsPredicate.java @@ -0,0 +1,31 @@ +package seedu.budgeteer.model.entry; + +import java.util.List; +import java.util.function.Predicate; + +import seedu.budgeteer.commons.util.StringUtil; + +/** + * Tests that a {@code Entry}'s {@code Date} matches any of the keywords given. + */ +public class DateContainsSpecifiedKeywordsPredicate implements Predicate { + private final List keywords; + + public DateContainsSpecifiedKeywordsPredicate(List keywords) { + this.keywords = keywords; + } + + @Override + public boolean test(Entry entry) { + return keywords.stream() + .anyMatch(keyword -> StringUtil.containsWordIgnoreCase(entry.getDate().value, keyword)); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof DateContainsSpecifiedKeywordsPredicate // instanceof handles nulls + && this.keywords.equals(((DateContainsSpecifiedKeywordsPredicate) other).keywords)); // state check + } + +} diff --git a/src/main/java/seedu/budgeteer/model/entry/DateIsWithinIntervalPredicate.java b/src/main/java/seedu/budgeteer/model/entry/DateIsWithinIntervalPredicate.java new file mode 100755 index 000000000000..1bb3fe4d1860 --- /dev/null +++ b/src/main/java/seedu/budgeteer/model/entry/DateIsWithinIntervalPredicate.java @@ -0,0 +1,64 @@ +package seedu.budgeteer.model.entry; + +import static seedu.budgeteer.commons.util.CollectionUtil.requireAllNonNull; +import static seedu.budgeteer.commons.util.DateUtil.isEarlierThan; +import static seedu.budgeteer.commons.util.DateUtil.isLaterThan; + +import java.util.function.Predicate; +import java.util.logging.Logger; + +import seedu.budgeteer.commons.core.LogsCenter; + +/** + * Tests that a {@code Entry}'s {@code Date} falls within the time interval from {@code startDate} to {@code endDate} + * This relationship is inclusive on both ends, meaning the date can be equal to {@code startDate} or {@code endDate} + */ +public class DateIsWithinIntervalPredicate implements Predicate { + private Logger logger = LogsCenter.getLogger(DateIsWithinIntervalPredicate.class); + + private final Date startDate; + private final Date endDate; + + public DateIsWithinIntervalPredicate(Date startDate, Date endDate) { + requireAllNonNull(startDate, endDate); + this.startDate = startDate; + this.endDate = endDate; + } + + public DateIsWithinIntervalPredicate(String startDate, String endDate) { + this.startDate = new Date(startDate); + this.endDate = new Date(endDate); + } + + public Date getStartDate() { + return startDate; + } + + public Date getEndDate() { + return endDate; + } + + /** + * Check if Start Date is smaller than/equal to End Date + * @return the boolean if StartDate is smaller than/equal to End Date + */ + public boolean isValidPredicate() { + return (isEarlierThan(startDate, endDate) || startDate.equals(endDate)); + } + + @Override + public boolean test(Entry entry) { + Date recordDate = entry.getDate(); + return recordDate.equals(startDate) || recordDate.equals(endDate) + || isLaterThan(recordDate, startDate) && isEarlierThan(recordDate, endDate); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof DateIsWithinIntervalPredicate // instanceof handles nulls + && startDate.equals(((DateIsWithinIntervalPredicate) other).startDate)) // start date check + && endDate.equals(((DateIsWithinIntervalPredicate) other).endDate); // end date check + } + +} diff --git a/src/main/java/seedu/budgeteer/model/entry/Entry.java b/src/main/java/seedu/budgeteer/model/entry/Entry.java new file mode 100644 index 000000000000..2f30fa495d66 --- /dev/null +++ b/src/main/java/seedu/budgeteer/model/entry/Entry.java @@ -0,0 +1,112 @@ +package seedu.budgeteer.model.entry; + +import static seedu.budgeteer.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +import seedu.budgeteer.model.tag.Tag; + +/** + * Represents a Entry in the budgeteer book. + * Guarantees: details are present and not null, field values are validated, immutable. + */ +public class Entry { + + // Identity fields + private final Name name; + + + // Data fields + private final Date date; + private final CashFlow cashFlow; + private final Set tags = new HashSet<>(); + + /** + * Every field must be present and not null. + */ + public Entry(Name name, Date date, CashFlow cashFlow, Set tags) { + requireAllNonNull(name, date, cashFlow, tags); + this.name = name; + this.date = date; + this.cashFlow = cashFlow; + this.tags.addAll(tags); + } + + public Name getName() { + return name; + } + + public Date getDate() { + return date; + } + + public CashFlow getCashFlow() { + return cashFlow; + } + + /** + * Returns an immutable tag set, which throws {@code UnsupportedOperationException} + * if modification is attempted. + */ + public Set getTags() { + return Collections.unmodifiableSet(tags); + } + + /** + * Returns true if both entrys of the same name have at least one other identity field that is the same. + * This defines a weaker notion of equality between two entrys. + */ + public boolean isSameEntry(Entry otherEntry) { + if (otherEntry == this) { + return true; + } + + return otherEntry != null + && otherEntry.getName().equals(getName()) + && (otherEntry.getDate().equals(getDate()) || otherEntry.getCashFlow().equals(getCashFlow())); + } + + /** + * Returns true if both entrys have the same identity and data fields. + * This defines a stronger notion of equality between two entrys. + */ + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof Entry)) { + return false; + } + + Entry otherEntry = (Entry) other; + return otherEntry.getName().equals(getName()) + && otherEntry.getDate().equals(getDate()) + && otherEntry.getCashFlow().equals(getCashFlow()) + && otherEntry.getTags().equals(getTags()); + } + + @Override + public int hashCode() { + // use this method for custom fields hashing instead of implementing your own + return Objects.hash(name, date, cashFlow, tags); + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append(getName()) + .append(" Date: ") + .append(getDate()) + .append(" CashFlow: ") + .append(getCashFlow()) + .append(" Tags: "); + getTags().forEach(builder::append); + return builder.toString(); + } + +} diff --git a/src/main/java/seedu/budgeteer/model/entry/EntryList.java b/src/main/java/seedu/budgeteer/model/entry/EntryList.java new file mode 100644 index 000000000000..9027fbfa7663 --- /dev/null +++ b/src/main/java/seedu/budgeteer/model/entry/EntryList.java @@ -0,0 +1,179 @@ +package seedu.budgeteer.model.entry; + +import static java.util.Objects.requireNonNull; +import static seedu.budgeteer.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.Iterator; +import java.util.List; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import seedu.budgeteer.commons.util.CompareUtil; +import seedu.budgeteer.logic.commands.DisplayCommand; +import seedu.budgeteer.model.entry.exceptions.EntryNotFoundException; + +/** + * A list of entrys that enforces uniqueness between its elements and does not allow nulls. + * A entry is considered unique by comparing using {@code Entry#isSameEntry(Entry)}. As such, adding and updating of + * entrys uses Entry#isSameEntry(Entry) for equality so as to ensure that the entry being added or updated is + * unique in terms of identity in the EntryList. However, the removal of a entry uses Entry#equals(Object) so + * as to ensure that the entry with exactly the same fields will be removed. + * + * Supports a minimal set of list operations. + * + * @see Entry#isSameEntry(Entry) + */ +public class EntryList implements Iterable { + + private final ObservableList internalList = FXCollections.observableArrayList(); + private final ObservableList internalUnmodifiableList = + FXCollections.unmodifiableObservableList(internalList); + + /** + * Returns true if the list contains an equivalent entry as the given argument. + */ + public boolean contains(Entry toCheck) { + requireNonNull(toCheck); + return internalList.stream().anyMatch(toCheck::isSameEntry); + } + + /** + * Adds a entry to the list. + * The entry must not already exist in the list. + */ + public void add(Entry toAdd) { + requireNonNull(toAdd); + /* + if (contains(toAdd)) { + throw new DuplicateEntryException(); + } + */ + internalList.add(toAdd); + } + + /** + * Replaces the entry {@code target} in the list with {@code editedEntry}. + * {@code target} must exist in the list. + * The entry identity of {@code editedEntry} must not be the same as another existing entry in the list. + */ + public void setEntry(Entry target, Entry editedEntry) { + requireAllNonNull(target, editedEntry); + + int index = internalList.indexOf(target); + if (index == -1) { + throw new EntryNotFoundException(); + } + /* + if (!target.isSameEntry(editedEntry) && contains(editedEntry)) { + throw new DuplicateEntryException(); + } + */ + internalList.set(index, editedEntry); + } + + /** + * Removes the equivalent entry from the list. + * The entry must exist in the list. + */ + public void remove(Entry toRemove) { + requireNonNull(toRemove); + if (!internalList.remove(toRemove)) { + throw new EntryNotFoundException(); + } + } + + public void setEntrys(EntryList replacement) { + requireNonNull(replacement); + internalList.setAll(replacement.internalList); + } + + /** + * Replaces the contents of this list with {@code entrys}. + * {@code entrys} must not contain duplicate entrys. + */ + public void setEntrys(List entrys) { + requireAllNonNull(entrys); + /* + if (!entrysAreUnique(entrys)) { + throw new DuplicateEntryException(); + } + */ + internalList.setAll(entrys); + } + + /** + * 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 EntryList // instanceof handles nulls + && internalList.equals(((EntryList) other).internalList)); + } + + @Override + public int hashCode() { + return internalList.hashCode(); + } + + /** + * Returns true if {@code entrys} contains only unique entrys. + */ + private boolean entrysAreUnique(List entrys) { + for (int i = 0; i < entrys.size() - 1; i++) { + for (int j = i + 1; j < entrys.size(); j++) { + if (entrys.get(i).isSameEntry(entrys.get(j))) { + return false; + } + } + } + return true; + } + + /** + * Sort the entries in the list according to their category, in ascending or descending order + * @param category category to sort the entries with + * @param ascending set to true to sort in ascending order, false for descending order + */ + public void sortEntrys(String category, Boolean ascending) { + switch (category) { + + case DisplayCommand.CATEGORY_NAME: + if (!ascending) { + internalList.sort(CompareUtil.compareNameAttribute().reversed()); + } else { + internalList.sort(CompareUtil.compareNameAttribute()); + } + break; + + case DisplayCommand.CATEGORY_DATE: + if (!ascending) { + internalList.sort(CompareUtil.compareDateAttribute().reversed()); + } else { + internalList.sort(CompareUtil.compareDateAttribute()); + } + break; + + case DisplayCommand.CATEGORY_CASHFLOW: + case DisplayCommand.CATEGORY_CASH: + if (!ascending) { + internalList.sort(CompareUtil.compareCashflowAttribute().reversed()); + } else { + internalList.sort(CompareUtil.compareCashflowAttribute()); + } + break; + + default: break; + } + + } +} diff --git a/src/main/java/seedu/address/model/person/Name.java b/src/main/java/seedu/budgeteer/model/entry/Name.java similarity index 73% rename from src/main/java/seedu/address/model/person/Name.java rename to src/main/java/seedu/budgeteer/model/entry/Name.java index 79244d71cf73..582bfb417eb0 100644 --- a/src/main/java/seedu/address/model/person/Name.java +++ b/src/main/java/seedu/budgeteer/model/entry/Name.java @@ -1,22 +1,23 @@ -package seedu.address.model.person; +package seedu.budgeteer.model.entry; import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; +import static seedu.budgeteer.commons.util.AppUtil.checkArgument; /** - * Represents a Person's name in the address book. + * Represents a Entry's name in the budgeteer 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, + * The first character of the budgeteer must not be a whitespace, * otherwise " " (a blank string) becomes a valid input. */ + //TODO: May need to change the constraints public static final String VALIDATION_REGEX = "[\\p{Alnum}][\\p{Alnum} ]*"; + public static final String MESSAGE_CONSTRAINTS = "Names can only contain alphanumeric characters and spaces, " + + "and it should not be blank"; public final String fullName; diff --git a/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java b/src/main/java/seedu/budgeteer/model/entry/NameContainsKeywordsPredicate.java similarity index 73% rename from src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java rename to src/main/java/seedu/budgeteer/model/entry/NameContainsKeywordsPredicate.java index c9b5868427ca..5ccf94e54b48 100644 --- a/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java +++ b/src/main/java/seedu/budgeteer/model/entry/NameContainsKeywordsPredicate.java @@ -1,14 +1,14 @@ -package seedu.address.model.person; +package seedu.budgeteer.model.entry; import java.util.List; import java.util.function.Predicate; -import seedu.address.commons.util.StringUtil; +import seedu.budgeteer.commons.util.StringUtil; /** - * Tests that a {@code Person}'s {@code Name} matches any of the keywords given. + * Tests that a {@code Entry}'s {@code Name} matches any of the keywords given. */ -public class NameContainsKeywordsPredicate implements Predicate { +public class NameContainsKeywordsPredicate implements Predicate { private final List keywords; public NameContainsKeywordsPredicate(List keywords) { @@ -16,9 +16,9 @@ public NameContainsKeywordsPredicate(List keywords) { } @Override - public boolean test(Person person) { + public boolean test(Entry entry) { return keywords.stream() - .anyMatch(keyword -> StringUtil.containsWordIgnoreCase(person.getName().fullName, keyword)); + .anyMatch(keyword -> StringUtil.containsWordIgnoreCase(entry.getName().fullName, keyword)); } @Override diff --git a/src/main/java/seedu/budgeteer/model/entry/Number.java b/src/main/java/seedu/budgeteer/model/entry/Number.java new file mode 100644 index 000000000000..e18b0727ae21 --- /dev/null +++ b/src/main/java/seedu/budgeteer/model/entry/Number.java @@ -0,0 +1,60 @@ +package seedu.budgeteer.model.entry; + +import static java.util.Objects.requireNonNull; +import static seedu.budgeteer.commons.util.AppUtil.checkArgument; + +/** + * Represents a Entry's number in the budgeteer book. + * Guarantees: immutable; is valid as declared in {@link #isValidNumber(String)} + */ +public class Number { + + /* + * The first character of the budgeteer must not be a whitespace, + * otherwise " " (a blank string) becomes a valid input. + */ + //TODO: May need to change the constraints + public static final String VALIDATION_REGEX = "[\\p{Digit}\\.][\\p{Digit}\\. ]*"; + public static final String MESSAGE_CONSTRAINTS = "Numbers can only contain numbers and potentially " + + "one decimal point, " + + "and it should not be blank"; + + public final String fullNumber; + + /** + * Constructs a {@code Number}. + * + * @param number A valid number. + */ + public Number(String number) { + requireNonNull(number); + checkArgument(isValidNumber(number), MESSAGE_CONSTRAINTS); + fullNumber = number; + } + + /** + * Returns true if a given string is a valid number. + */ + public static boolean isValidNumber(String test) { + return test.matches(VALIDATION_REGEX); + } + + + @Override + public String toString() { + return fullNumber; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Number // instanceof handles nulls + && fullNumber.equals(((Number) other).fullNumber)); // state check + } + + @Override + public int hashCode() { + return fullNumber.hashCode(); + } + +} diff --git a/src/main/java/seedu/budgeteer/model/entry/ReportEntryList.java b/src/main/java/seedu/budgeteer/model/entry/ReportEntryList.java new file mode 100644 index 000000000000..1b96ba009d81 --- /dev/null +++ b/src/main/java/seedu/budgeteer/model/entry/ReportEntryList.java @@ -0,0 +1,117 @@ +package seedu.budgeteer.model.entry; + +import java.util.HashMap; +import java.util.Set; + +import javafx.collections.ObservableList; + +import seedu.budgeteer.model.tag.Tag; + + +/** + * This class represents a list containing all Summary objects computed from a given list of records + * and a predicate criteria. The internal implementation is a HashMap but it returns a list + * and implements only list functions + */ +public class ReportEntryList { + + private Double total; + private Double totalIncome; + private Double totalExpense; + private ObservableList filteredEntries; + private HashMap expenseCompositionMap; + private HashMap incomeCompositionMap; + + public ReportEntryList(ObservableList filteredEntries) { + this.total = 0.0; + this.totalIncome = 0.0; + this.totalExpense = 0.0; + this.filteredEntries = filteredEntries; + + updateTotals(); + } + + /** + * Returns the simple class name of the identifier used to categorise each entry in list + */ + public Double getTotal() { + return this.total; + } + + public Double getTotalIncome() { + return this.totalIncome; + } + + public Double getTotalExpense() { + return this.totalExpense; + } + + public HashMap getExpenseCompositionMap() { + return this.expenseCompositionMap; + } + + public HashMap getIncomeCompositionMap() { + return this.incomeCompositionMap; + } + + + /** + * Returns the size of the internal map + */ + public int size() { + return this.filteredEntries.size(); + } + + /** + * Checks if the internal map is empty + * + * @return true if empty and false if otherwise + */ + public boolean isEmpty() { + return this.filteredEntries.isEmpty(); + } + + + /** + * Update the total moneyflow, total income and total expense + */ + private void updateTotals() { + HashMap incomeComposition = new HashMap(); + HashMap expenseComposition = new HashMap(); + + for (Entry i : this.filteredEntries) { + CashFlow icf = i.getCashFlow(); + Set iTags = i.getTags(); + String tagStr = iTags.toString(); + if (tagStr.equalsIgnoreCase("[]")) { + tagStr = "Uncategorized"; + } + tagStr = tagStr.replaceAll("\\[", "").replaceAll("\\]", ""); + + Double value = icf.valueDouble; + total += value; + if (value < 0) { + totalExpense += (-1 * value); + if (expenseComposition.containsKey(tagStr)) { + Double oldVal = expenseComposition.get(tagStr); + expenseComposition.replace(tagStr, (oldVal + (-1 * value))); + } else { + expenseComposition.put(tagStr, (-1 * value)); + } + } else { + totalIncome += value; + if (incomeComposition.containsKey(tagStr)) { + Double oldVal = incomeComposition.get(tagStr); + incomeComposition.replace(tagStr, (oldVal + value)); + } else { + incomeComposition.put(tagStr, value); + } + } + } + + this.incomeCompositionMap = incomeComposition; + this.expenseCompositionMap = expenseComposition; + } + + +} diff --git a/src/main/java/seedu/budgeteer/model/entry/TagContainsSpecifiedKeywordsPredicate.java b/src/main/java/seedu/budgeteer/model/entry/TagContainsSpecifiedKeywordsPredicate.java new file mode 100644 index 000000000000..2c6a6e109056 --- /dev/null +++ b/src/main/java/seedu/budgeteer/model/entry/TagContainsSpecifiedKeywordsPredicate.java @@ -0,0 +1,37 @@ +package seedu.budgeteer.model.entry; + +import java.util.List; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import seedu.budgeteer.model.tag.Tag; + + +/** + * Tests that a {@code Entry}'s {@code Tag} matches any of the keywords given. + */ +public class TagContainsSpecifiedKeywordsPredicate implements Predicate { + private final List keywords; + + public TagContainsSpecifiedKeywordsPredicate(List keywords) { + this.keywords = keywords; + } + + @Override + public boolean test(Entry entry) { + Set tags = entry.getTags(); + List tagList = tags.stream().collect(Collectors.toList()); // Converts Set to List + + return keywords.stream() + .anyMatch(keyword -> tagList.stream().anyMatch(tagname -> keyword.contains(tagname.toString())) & true); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof TagContainsSpecifiedKeywordsPredicate // instanceof handles nulls + && this.keywords.equals(((TagContainsSpecifiedKeywordsPredicate) other).keywords)); // state check + } + +} diff --git a/src/main/java/seedu/budgeteer/model/entry/exceptions/DuplicateEntryException.java b/src/main/java/seedu/budgeteer/model/entry/exceptions/DuplicateEntryException.java new file mode 100644 index 000000000000..c67f5ee028f3 --- /dev/null +++ b/src/main/java/seedu/budgeteer/model/entry/exceptions/DuplicateEntryException.java @@ -0,0 +1,11 @@ +package seedu.budgeteer.model.entry.exceptions; + +/** + * Signals that the operation will result in duplicate Persons (Persons are considered duplicates if they have the same + * identity). + */ +public class DuplicateEntryException extends RuntimeException { + public DuplicateEntryException() { + super("Operation would result in duplicate entrys"); + } +} diff --git a/src/main/java/seedu/budgeteer/model/entry/exceptions/EntryNotFoundException.java b/src/main/java/seedu/budgeteer/model/entry/exceptions/EntryNotFoundException.java new file mode 100644 index 000000000000..9b335d8c773f --- /dev/null +++ b/src/main/java/seedu/budgeteer/model/entry/exceptions/EntryNotFoundException.java @@ -0,0 +1,6 @@ +package seedu.budgeteer.model.entry.exceptions; + +/** + * Signals that the operation is unable to find the specified entry. + */ +public class EntryNotFoundException extends RuntimeException {} diff --git a/src/main/java/seedu/budgeteer/model/summary/CategoryStatistic.java b/src/main/java/seedu/budgeteer/model/summary/CategoryStatistic.java new file mode 100755 index 000000000000..73f99737390f --- /dev/null +++ b/src/main/java/seedu/budgeteer/model/summary/CategoryStatistic.java @@ -0,0 +1,73 @@ +package seedu.budgeteer.model.summary; + +import java.util.Set; + +import seedu.budgeteer.model.entry.CashFlow; +import seedu.budgeteer.model.entry.Entry; +import seedu.budgeteer.model.tag.Tag; + + + +/** + * This class represents a in memory model of the statistic of a category. It contains totalIncome and totalExpenses. + */ +public class CategoryStatistic { + + private Set tags; + private Double totalIncome = 0.0; + private Double totalExpense = 0.0; + + public CategoryStatistic(Entry entry) { + tags = entry.getTags(); + if (entry.getCashFlow().toDouble() == 0) { + throw new IllegalStateException(CashFlow.MESSAGE_CONSTRAINTS); + } + if (isExpense(entry)) { + totalExpense = Math.abs(entry.getCashFlow().toDouble()); + } else { + totalIncome = Math.abs(entry.getCashFlow().toDouble()); + } + } + + public Set getTags() { + return tags; + } + + public Double getTotalIncome() { + return totalIncome; + } + + public Double getTotalExpense() { + return totalExpense; + } + + /** Adds {@code CashFlow} of entry to {@code CategoryStatistic} */ + public void add(Entry entry) { + assert(entry.getTags().equals(tags)); + if (isExpense(entry)) { + totalExpense += Math.abs(entry.getCashFlow().toDouble()); + if (totalExpense > CashFlow.MAX_CASH) { + throw new IllegalArgumentException(CashFlow.MESSAGE_CONSTRAINTS); + } + } else { + totalIncome += Math.abs(entry.getCashFlow().toDouble()); + if (totalIncome > CashFlow.MAX_CASH) { + throw new IllegalArgumentException(CashFlow.MESSAGE_CONSTRAINTS); + } + } + } + + private boolean isExpense(Entry entry) { + return entry.getCashFlow().toDouble() < 0; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof CategoryStatistic // instanceof handles nulls + && tags.equals(((CategoryStatistic) other).tags) + && totalIncome.equals(((CategoryStatistic) other).totalIncome) + && totalExpense.equals(((CategoryStatistic) other).totalExpense)); + } +} + diff --git a/src/main/java/seedu/budgeteer/model/summary/CategoryStatisticsList.java b/src/main/java/seedu/budgeteer/model/summary/CategoryStatisticsList.java new file mode 100755 index 000000000000..7ea5580edef5 --- /dev/null +++ b/src/main/java/seedu/budgeteer/model/summary/CategoryStatisticsList.java @@ -0,0 +1,64 @@ +package seedu.budgeteer.model.summary; + +import static java.util.Objects.requireNonNull; + +import java.util.HashMap; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +import seedu.budgeteer.model.entry.Entry; +import seedu.budgeteer.model.tag.Tag; + +/** + * This class represents a list to store the statistics of each category within a certain time period. + */ +public class CategoryStatisticsList { + + private HashMap, CategoryStatistic> categoryStats; + + public CategoryStatisticsList(List entryList) { + requireNonNull(entryList); + categoryStats = new HashMap<>(); + for (Entry r : entryList) { + Set tags = r.getTags(); + addToCategoryStatistics(r, tags); + } + } + + /** + * Adds the entry into the internal hashMap + * @param entry entry to be added + * @param tags the key of the addition + */ + private void addToCategoryStatistics(Entry entry, Set tags) { + if (categoryStats.containsKey(tags)) { + categoryStats.get(tags).add(entry); + } else { + categoryStats.put(entry.getTags(), new CategoryStatistic(entry)); + } + } + + /** + * Returns the contents of the internal {@link HashMap} as a read only {@link ObservableList} + */ + public ObservableList getReadOnlyStatsList() { + List statsList = categoryStats.keySet().stream() + .map(s -> categoryStats.get(s)).collect(Collectors.toList()); + return FXCollections.unmodifiableObservableList(FXCollections.observableList(statsList)); + } + + public HashMap, CategoryStatistic> getCategoryStatsMap() { + return categoryStats; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof CategoryStatisticsList // instanceof handles nulls + && categoryStats.equals(((CategoryStatisticsList) other).categoryStats)); + } +} diff --git a/src/main/java/seedu/budgeteer/model/summary/Summary.java b/src/main/java/seedu/budgeteer/model/summary/Summary.java new file mode 100755 index 000000000000..684019cf9f7a --- /dev/null +++ b/src/main/java/seedu/budgeteer/model/summary/Summary.java @@ -0,0 +1,95 @@ +package seedu.budgeteer.model.summary; + +import static java.util.Objects.requireNonNull; +import static seedu.budgeteer.commons.util.CollectionUtil.requireAllNonNull; + +import seedu.budgeteer.commons.util.MoneyUtil; +import seedu.budgeteer.model.entry.CashFlow; +import seedu.budgeteer.model.entry.Entry; + +/** + * This class represents a summary in EntriesBook which records can be added to. Type T is the class of the + * identifier that the summary is associated with + */ +public class Summary { + + private Identifier identifier; + private CashFlow totalExpense; + private CashFlow totalIncome; + private CashFlow total; + + public Summary(Entry entry, Identifier identifier) { + requireAllNonNull(entry, identifier); + this.identifier = identifier; + CashFlow money = entry.getCashFlow(); + if (isExpense(money)) { + totalExpense = money; + totalIncome = new CashFlow(CashFlow.REPRESENTATION_ZERO); + } else { + totalIncome = money; + totalExpense = new CashFlow(CashFlow.REPRESENTATION_ZERO); + } + total = money; + } + + public Summary(Identifier identifier, CashFlow totalExpense, CashFlow totalIncome, CashFlow total) { + requireAllNonNull(identifier, totalExpense, totalIncome, total); + this.identifier = identifier; + this.totalExpense = totalExpense; + this.totalIncome = totalIncome; + this.total = total; + } + + public Identifier getIdentifier() { + return identifier; + } + + public CashFlow getTotalExpense() { + return totalExpense; + } + + public CashFlow getTotalIncome() { + return totalIncome; + } + + public CashFlow getTotal() { + return total; + } + + /** + * Adds entry into the summary object + * @param entry entry to be added + */ + public void add(Entry entry) { + requireNonNull(entry); + CashFlow money = entry.getCashFlow(); + if (isExpense(money)) { + totalExpense = MoneyUtil.add(totalExpense, money); + } else { + totalIncome = MoneyUtil.add(totalIncome, money); + } + total = MoneyUtil.add(total, money); + } + + private boolean isExpense(CashFlow money) { + return money.toDouble() < 0; + } + + @Override + public boolean equals(Object other) { + return this == other // short circuit if same object + || (other instanceof Summary // instanceof handles nulls + && identifier.equals(((Summary) other).identifier) + && totalExpense.equals(((Summary) other).totalExpense) + && totalIncome.equals(((Summary) other).totalIncome) + && total.equals(((Summary) other).total)); + } + + @Override + public String toString() { + return identifier.getClass().getSimpleName() + ": " + identifier + "\n" + + "Total Expense: " + totalExpense + "\n" + + "Total Income: " + totalIncome + "\n" + + "Total: " + total; + } +} diff --git a/src/main/java/seedu/budgeteer/model/summary/SummaryByCategoryList.java b/src/main/java/seedu/budgeteer/model/summary/SummaryByCategoryList.java new file mode 100755 index 000000000000..c540e55516d2 --- /dev/null +++ b/src/main/java/seedu/budgeteer/model/summary/SummaryByCategoryList.java @@ -0,0 +1,108 @@ +package seedu.budgeteer.model.summary; + +import static java.util.Objects.requireNonNull; + +import java.util.HashMap; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +import seedu.budgeteer.commons.util.CompareUtil; +import seedu.budgeteer.model.entry.CashFlow; +import seedu.budgeteer.model.entry.Entry; +import seedu.budgeteer.model.tag.Tag; +import seedu.budgeteer.ui.SummaryEntry; + + + +/** + * This class represents a list containing all Summary objects computed from a given list of records + * and a predicate criteria. The internal implementation is a HashMap but it returns a list + * and implements only list functions + */ +public class SummaryByCategoryList extends SummaryList { + private HashMap, Summary>> summaryMap = new HashMap<>(); + + public SummaryByCategoryList(List entryList) { + super(); + requireNonNull(entryList); + for (Entry r : entryList) { + addRecordToMap(r); + updateTotals(r); + } + } + + @Override + public ObservableList getSummaryList() { + + List list = summaryMap.keySet().stream().sorted(CompareUtil.compareTags()) + .map(k -> SummaryEntry.convertToUiFriendly(summaryMap.get(k))) + .collect(Collectors.toList()); + return FXCollections.observableList(list); + } + + public HashMap, Summary>> getSummaryMap() { + return summaryMap; + } + + /** Adds a entry to the {@code summaryMap} while following some rules. + * If there exists a summary with {@code Date} of entry, then entry is added to the summary. + * Else, it creates a summary with the details of the entry. + * @param entry given entry + * @see Summary#add(Entry) + */ + @Override + protected void addRecordToMap(Entry entry) { + Set tags = entry.getTags(); + if (summaryMap.containsKey(tags)) { + summaryMap.get(tags).add(entry); + } else { + summaryMap.put(tags, new Summary<>(entry, tags)); + } + } + + @Override + protected void updateTotals(Entry entry) { + super.updateTotals(entry); + } + + @Override + public int size() { + return summaryMap.size(); + } + + @Override + public boolean isEmpty() { + return summaryMap.size() == 0; + } + + @Override + public CashFlow getTotal() { + return total; + } + + @Override + public CashFlow getTotalIncome() { + return totalIncome; + } + + @Override + public CashFlow getTotalExpense() { + return totalExpense; + } + + @Override + public String getIdentifierName() { + return "Category"; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof SummaryByCategoryList // instanceof handles nulls + && summaryMap.equals(((SummaryByCategoryList) other).summaryMap)); + } +} diff --git a/src/main/java/seedu/budgeteer/model/summary/SummaryByDateList.java b/src/main/java/seedu/budgeteer/model/summary/SummaryByDateList.java new file mode 100755 index 000000000000..0c55eec08464 --- /dev/null +++ b/src/main/java/seedu/budgeteer/model/summary/SummaryByDateList.java @@ -0,0 +1,102 @@ +package seedu.budgeteer.model.summary; + +import static java.util.Objects.requireNonNull; + +import java.util.HashMap; +import java.util.List; +import java.util.stream.Collectors; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +import seedu.budgeteer.commons.util.CompareUtil; +import seedu.budgeteer.model.entry.CashFlow; +import seedu.budgeteer.model.entry.Date; +import seedu.budgeteer.model.entry.Entry; +import seedu.budgeteer.ui.SummaryEntry; + + + +/** + * This class represents a list containing all Summary objects computed from a given list of records + * and a predicate criteria. The internal implementation is a HashMap but it returns a list + * and implements only list functions + */ +public class SummaryByDateList extends SummaryList { + private HashMap> summaryMap = new HashMap<>(); + + public SummaryByDateList(List entryList) { + super(); + requireNonNull(entryList); + for (Entry r : entryList) { + addRecordToMap(r); + updateTotals(r); + } + } + + @Override + public ObservableList getSummaryList() { + + List list = summaryMap.keySet().stream().sorted(CompareUtil.compareDate()) + .map(k -> SummaryEntry.convertToUiFriendly(summaryMap.get(k))) + .collect(Collectors.toList()); + return FXCollections.observableList(list); + } + + public HashMap> getSummaryMap() { + return summaryMap; + } + + + @Override + protected void addRecordToMap(Entry entry) { + Date date = entry.getDate(); + if (summaryMap.containsKey(date)) { + summaryMap.get(date).add(entry); + } else { + summaryMap.put(date, new Summary<>(entry, date)); + } + } + + @Override + protected void updateTotals(Entry entry) { + super.updateTotals(entry); + } + + @Override + public int size() { + return summaryMap.size(); + } + + @Override + public boolean isEmpty() { + return summaryMap.size() == 0; + } + + @Override + public CashFlow getTotal() { + return total; + } + + @Override + public CashFlow getTotalIncome() { + return totalIncome; + } + + @Override + public CashFlow getTotalExpense() { + return totalExpense; + } + + @Override + public String getIdentifierName() { + return Date.class.getSimpleName(); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof SummaryByDateList // instanceof handles nulls + && summaryMap.equals(((SummaryByDateList) other).summaryMap)); + } +} diff --git a/src/main/java/seedu/budgeteer/model/summary/SummaryList.java b/src/main/java/seedu/budgeteer/model/summary/SummaryList.java new file mode 100755 index 000000000000..3f241758671c --- /dev/null +++ b/src/main/java/seedu/budgeteer/model/summary/SummaryList.java @@ -0,0 +1,76 @@ +package seedu.budgeteer.model.summary; + +import javafx.collections.ObservableList; +import seedu.budgeteer.commons.util.MoneyUtil; +import seedu.budgeteer.model.entry.CashFlow; +import seedu.budgeteer.model.entry.Entry; +import seedu.budgeteer.ui.SummaryEntry; + +/** + * This class represents a list containing all Summary objects computed from a given list of records + * and a predicate criteria. The internal implementation is a HashMap but it returns a list + * and implements only list functions + */ +public abstract class SummaryList { + + protected CashFlow total; + protected CashFlow totalIncome; + protected CashFlow totalExpense; + + public SummaryList() { + total = new CashFlow("-0"); + totalIncome = new CashFlow("-0"); + totalExpense = new CashFlow("-0"); + } + + /** + * Returns the simple class name of the identifier used to categorise each entry in list + */ + public abstract String getIdentifierName(); + + public abstract CashFlow getTotal(); + + public abstract CashFlow getTotalIncome(); + + public abstract CashFlow getTotalExpense(); + + /** + * Returns the size of the internal map + */ + public abstract int size(); + + /** + * Checks if the internal map is empty + * @return true if empty and false if otherwise + */ + public abstract boolean isEmpty(); + + /** Adds a entry to the {@code summaryMap} while following some rules. + * If there exists a summary with {@code Date} of entry, then entry is added to the summary. + * Else, it creates a summary with the details of the entry. + * @param entry given entry + * @see Summary#add(Entry) + */ + protected abstract void addRecordToMap(Entry entry); + + /** + * Converts internal map to a list of SummaryEntry objects and returns a read-only copy of that list + * @return read-only list of SummaryEntry objects + */ + public abstract ObservableList getSummaryList(); + + /** Update the total moneyflow, total income and total expense */ + protected void updateTotals(Entry entry) { + CashFlow money = entry.getCashFlow(); + if (isExpense(money)) { + totalExpense = MoneyUtil.add(totalExpense, money); + } else { + totalIncome = MoneyUtil.add(totalIncome, money); + } + total = MoneyUtil.add(total, money); + } + + private boolean isExpense(CashFlow money) { + return money.toDouble() < 0; + } +} diff --git a/src/main/java/seedu/address/model/tag/Tag.java b/src/main/java/seedu/budgeteer/model/tag/Tag.java similarity index 89% rename from src/main/java/seedu/address/model/tag/Tag.java rename to src/main/java/seedu/budgeteer/model/tag/Tag.java index b0ea7e7dad7f..e9d171e3177f 100644 --- a/src/main/java/seedu/address/model/tag/Tag.java +++ b/src/main/java/seedu/budgeteer/model/tag/Tag.java @@ -1,10 +1,10 @@ -package seedu.address.model.tag; +package seedu.budgeteer.model.tag; import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; +import static seedu.budgeteer.commons.util.AppUtil.checkArgument; /** - * Represents a Tag in the address book. + * Represents a Tag in the budgeteer book. * Guarantees: immutable; name is valid as declared in {@link #isValidTagName(String)} */ public class Tag { diff --git a/src/main/java/seedu/budgeteer/model/util/SampleDataUtil.java b/src/main/java/seedu/budgeteer/model/util/SampleDataUtil.java new file mode 100644 index 000000000000..2864411f7a73 --- /dev/null +++ b/src/main/java/seedu/budgeteer/model/util/SampleDataUtil.java @@ -0,0 +1,53 @@ +package seedu.budgeteer.model.util; + +import java.util.Arrays; +import java.util.Set; +import java.util.stream.Collectors; + +import seedu.budgeteer.model.EntriesBook; +import seedu.budgeteer.model.ReadOnlyEntriesBook; +import seedu.budgeteer.model.entry.CashFlow; +import seedu.budgeteer.model.entry.Date; +import seedu.budgeteer.model.entry.Entry; +import seedu.budgeteer.model.entry.Name; +import seedu.budgeteer.model.tag.Tag; + +/** + * Contains utility methods for populating {@code EntriesBook} with sample data. + */ +public class SampleDataUtil { + public static Entry[] getSampleEntrys() { + return new Entry[] { + new Entry(new Name("Salary from Alex Yeoh"), new Date("11-11-2019"), CashFlow.getCashFlow("+100"), + getTagSet("friends")), + new Entry(new Name("Lunch with Bernice Yu"), new Date("12-12-2018"), CashFlow.getCashFlow("+100"), + getTagSet("colleagues", "friends")), + new Entry(new Name("Burger with Charlotte Oliveiro"), new Date("11-11-2019"), CashFlow.getCashFlow("+100"), + getTagSet("neighbours")), + new Entry(new Name("School Loan"), new Date("12-12-2018"), CashFlow.getCashFlow("+100"), + getTagSet("personal")), + new Entry(new Name("Income"), new Date("12-12-2019"), CashFlow.getCashFlow("+100"), + getTagSet("work")), + new Entry(new Name("Dinner with Roy Balakrishnan"), new Date("11-11-2019"), CashFlow.getCashFlow("+100"), + getTagSet("colleagues")) + }; + } + + public static ReadOnlyEntriesBook getSampleAddressBook() { + EntriesBook sampleAb = new EntriesBook(); + for (Entry sampleEntry : getSampleEntrys()) { + sampleAb.addEntry(sampleEntry); + } + 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/budgeteer/storage/BudgeteerStorage.java similarity index 52% rename from src/main/java/seedu/address/storage/AddressBookStorage.java rename to src/main/java/seedu/budgeteer/storage/BudgeteerStorage.java index 4599182b3f92..54e503245d68 100644 --- a/src/main/java/seedu/address/storage/AddressBookStorage.java +++ b/src/main/java/seedu/budgeteer/storage/BudgeteerStorage.java @@ -1,16 +1,17 @@ -package seedu.address.storage; +package seedu.budgeteer.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.budgeteer.commons.exceptions.DataConversionException; +import seedu.budgeteer.model.EntriesBook; +import seedu.budgeteer.model.ReadOnlyEntriesBook; /** - * Represents a storage for {@link seedu.address.model.AddressBook}. + * Represents a storage for {@link EntriesBook}. */ -public interface AddressBookStorage { +public interface BudgeteerStorage { /** * Returns the file path of the data file. @@ -18,28 +19,28 @@ public interface AddressBookStorage { Path getAddressBookFilePath(); /** - * Returns AddressBook data as a {@link ReadOnlyAddressBook}. + * Returns EntriesBook data as a {@link ReadOnlyEntriesBook}. * 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; + Optional readEntriesBook() throws DataConversionException, IOException; /** * @see #getAddressBookFilePath() */ - Optional readAddressBook(Path filePath) throws DataConversionException, IOException; + Optional readEntriesBook(Path filePath) throws DataConversionException, IOException; /** - * Saves the given {@link ReadOnlyAddressBook} to the storage. + * Saves the given {@link ReadOnlyEntriesBook} 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; + void saveAddressBook(ReadOnlyEntriesBook addressBook) throws IOException; /** - * @see #saveAddressBook(ReadOnlyAddressBook) + * @see #saveAddressBook(ReadOnlyEntriesBook) */ - void saveAddressBook(ReadOnlyAddressBook addressBook, Path filePath) throws IOException; + void saveAddressBook(ReadOnlyEntriesBook addressBook, Path filePath) throws IOException; } diff --git a/src/main/java/seedu/budgeteer/storage/JsonAdaptedEntry.java b/src/main/java/seedu/budgeteer/storage/JsonAdaptedEntry.java new file mode 100644 index 000000000000..17bb28462360 --- /dev/null +++ b/src/main/java/seedu/budgeteer/storage/JsonAdaptedEntry.java @@ -0,0 +1,99 @@ +package seedu.budgeteer.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.budgeteer.commons.exceptions.IllegalValueException; +import seedu.budgeteer.model.entry.CashFlow; +import seedu.budgeteer.model.entry.Date; +import seedu.budgeteer.model.entry.Entry; +import seedu.budgeteer.model.entry.Name; +import seedu.budgeteer.model.tag.Tag; + +/** + * Jackson-friendly version of {@link Entry}. + */ +class JsonAdaptedEntry { + + public static final String MISSING_FIELD_MESSAGE_FORMAT = "Entry's %s field is missing!"; + + private final String name; + private final String date; + private final String cashFlow; + private final List tagged = new ArrayList<>(); + + /** + * Constructs a {@code JsonAdaptedEntry} with the given entry details. + */ + @JsonCreator + public JsonAdaptedEntry(@JsonProperty("name") String name, @JsonProperty("date") String date, + @JsonProperty("cashFlow") String cashFlow, + @JsonProperty("tagged") List tagged) { + this.name = name; + this.date = date; + this.cashFlow = cashFlow; + if (tagged != null) { + this.tagged.addAll(tagged); + } + } + + /** + * Converts a given {@code Entry} into this class for Jackson use. + */ + public JsonAdaptedEntry(Entry source) { + name = source.getName().fullName; + date = source.getDate().value; + cashFlow = source.getCashFlow().value; + tagged.addAll(source.getTags().stream() + .map(JsonAdaptedTag::new) + .collect(Collectors.toList())); + } + + /** + * Converts this Jackson-friendly adapted entry object into the model's {@code Entry} object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted entry. + */ + public Entry toModelType() throws IllegalValueException { + final List entryTags = new ArrayList<>(); + for (JsonAdaptedTag tag : tagged) { + entryTags.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 (date == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Date.class.getSimpleName())); + } + if (!Date.isValidDateFormat(date)) { + throw new IllegalValueException(Date.MESSAGE_DATE_CONSTRAINTS); + } + final Date modelDate = new Date(date); + + if (cashFlow == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, + CashFlow.class.getSimpleName())); + } + if (!CashFlow.isValidCashFlow(cashFlow)) { + throw new IllegalValueException(CashFlow.MESSAGE_CONSTRAINTS); + } + final CashFlow modelCashFlow = CashFlow.getCashFlow(cashFlow); + + + final Set modelTags = new HashSet<>(entryTags); + return new Entry(modelName, modelDate, modelCashFlow, modelTags); + } + +} diff --git a/src/main/java/seedu/address/storage/JsonAdaptedTag.java b/src/main/java/seedu/budgeteer/storage/JsonAdaptedTag.java similarity index 88% rename from src/main/java/seedu/address/storage/JsonAdaptedTag.java rename to src/main/java/seedu/budgeteer/storage/JsonAdaptedTag.java index 0df22bdb7546..0dd269b26636 100644 --- a/src/main/java/seedu/address/storage/JsonAdaptedTag.java +++ b/src/main/java/seedu/budgeteer/storage/JsonAdaptedTag.java @@ -1,10 +1,10 @@ -package seedu.address.storage; +package seedu.budgeteer.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; +import seedu.budgeteer.commons.exceptions.IllegalValueException; +import seedu.budgeteer.model.tag.Tag; /** * Jackson-friendly version of {@link Tag}. diff --git a/src/main/java/seedu/address/storage/JsonAddressBookStorage.java b/src/main/java/seedu/budgeteer/storage/JsonBudgeteerStorage.java similarity index 54% rename from src/main/java/seedu/address/storage/JsonAddressBookStorage.java rename to src/main/java/seedu/budgeteer/storage/JsonBudgeteerStorage.java index dfab9daaa0d3..9b087d6c3613 100644 --- a/src/main/java/seedu/address/storage/JsonAddressBookStorage.java +++ b/src/main/java/seedu/budgeteer/storage/JsonBudgeteerStorage.java @@ -1,4 +1,4 @@ -package seedu.address.storage; +package seedu.budgeteer.storage; import static java.util.Objects.requireNonNull; @@ -7,23 +7,23 @@ 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; +import seedu.budgeteer.commons.core.LogsCenter; +import seedu.budgeteer.commons.exceptions.DataConversionException; +import seedu.budgeteer.commons.exceptions.IllegalValueException; +import seedu.budgeteer.commons.util.FileUtil; +import seedu.budgeteer.commons.util.JsonUtil; +import seedu.budgeteer.model.ReadOnlyEntriesBook; /** - * A class to access AddressBook data stored as a json file on the hard disk. + * A class to access EntriesBook data stored as a json file on the hard disk. */ -public class JsonAddressBookStorage implements AddressBookStorage { +public class JsonBudgeteerStorage implements BudgeteerStorage { - private static final Logger logger = LogsCenter.getLogger(JsonAddressBookStorage.class); + private static final Logger logger = LogsCenter.getLogger(JsonBudgeteerStorage.class); private Path filePath; - public JsonAddressBookStorage(Path filePath) { + public JsonBudgeteerStorage(Path filePath) { this.filePath = filePath; } @@ -32,21 +32,21 @@ public Path getAddressBookFilePath() { } @Override - public Optional readAddressBook() throws DataConversionException { - return readAddressBook(filePath); + public Optional readEntriesBook() throws DataConversionException { + return readEntriesBook(filePath); } /** - * Similar to {@link #readAddressBook()}. + * Similar to {@link #readEntriesBook()}. * * @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 { + public Optional readEntriesBook(Path filePath) throws DataConversionException { requireNonNull(filePath); - Optional jsonAddressBook = JsonUtil.readJsonFile( - filePath, JsonSerializableAddressBook.class); + Optional jsonAddressBook = JsonUtil.readJsonFile( + filePath, JsonSerializableBudgeteer.class); if (!jsonAddressBook.isPresent()) { return Optional.empty(); } @@ -60,21 +60,21 @@ public Optional readAddressBook(Path filePath) throws DataC } @Override - public void saveAddressBook(ReadOnlyAddressBook addressBook) throws IOException { + public void saveAddressBook(ReadOnlyEntriesBook addressBook) throws IOException { saveAddressBook(addressBook, filePath); } /** - * Similar to {@link #saveAddressBook(ReadOnlyAddressBook)}. + * Similar to {@link #saveAddressBook(ReadOnlyEntriesBook)}. * * @param filePath location of the data. Cannot be null. */ - public void saveAddressBook(ReadOnlyAddressBook addressBook, Path filePath) throws IOException { + public void saveAddressBook(ReadOnlyEntriesBook addressBook, Path filePath) throws IOException { requireNonNull(addressBook); requireNonNull(filePath); FileUtil.createIfMissing(filePath); - JsonUtil.saveJsonFile(new JsonSerializableAddressBook(addressBook), filePath); + JsonUtil.saveJsonFile(new JsonSerializableBudgeteer(addressBook), filePath); } } diff --git a/src/main/java/seedu/budgeteer/storage/JsonSerializableBudgeteer.java b/src/main/java/seedu/budgeteer/storage/JsonSerializableBudgeteer.java new file mode 100644 index 000000000000..f2bd0afb11c4 --- /dev/null +++ b/src/main/java/seedu/budgeteer/storage/JsonSerializableBudgeteer.java @@ -0,0 +1,60 @@ +package seedu.budgeteer.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.budgeteer.commons.exceptions.IllegalValueException; +import seedu.budgeteer.model.EntriesBook; +import seedu.budgeteer.model.ReadOnlyEntriesBook; +import seedu.budgeteer.model.entry.Entry; + +/** + * An Immutable EntriesBook that is serializable to JSON format. + */ +@JsonRootName(value = "addressbook") +class JsonSerializableBudgeteer { + + public static final String MESSAGE_DUPLICATE_ENTRY = "Entrys list contains duplicate entry(s)."; + + private final List entrys = new ArrayList<>(); + + /** + * Constructs a {@code JsonSerializableBudgeteer} with the given entrys. + */ + @JsonCreator + public JsonSerializableBudgeteer(@JsonProperty("entrys") List entrys) { + this.entrys.addAll(entrys); + } + + /** + * Converts a given {@code ReadOnlyEntriesBook} into this class for Jackson use. + * + * @param source future changes to this will not affect the created {@code JsonSerializableBudgeteer}. + */ + public JsonSerializableBudgeteer(ReadOnlyEntriesBook source) { + entrys.addAll(source.getEntryList().stream().map(JsonAdaptedEntry::new).collect(Collectors.toList())); + } + + /** + * Converts this budgeteer book into the model's {@code EntriesBook} object. + * + * @throws IllegalValueException if there were any data constraints violated. + */ + public EntriesBook toModelType() throws IllegalValueException { + EntriesBook entriesBook = new EntriesBook(); + for (JsonAdaptedEntry jsonAdaptedEntry : entrys) { + Entry entry = jsonAdaptedEntry.toModelType(); + //if (entriesBook.hasEntry(entry)) { + // throw new IllegalValueException(MESSAGE_DUPLICATE_ENTRY); + //} + entriesBook.addEntry(entry); + } + return entriesBook; + } + +} diff --git a/src/main/java/seedu/address/storage/JsonUserPrefsStorage.java b/src/main/java/seedu/budgeteer/storage/JsonUserPrefsStorage.java similarity index 82% rename from src/main/java/seedu/address/storage/JsonUserPrefsStorage.java rename to src/main/java/seedu/budgeteer/storage/JsonUserPrefsStorage.java index bc2bbad84aa3..e7df80d6d3b6 100644 --- a/src/main/java/seedu/address/storage/JsonUserPrefsStorage.java +++ b/src/main/java/seedu/budgeteer/storage/JsonUserPrefsStorage.java @@ -1,13 +1,13 @@ -package seedu.address.storage; +package seedu.budgeteer.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 seedu.budgeteer.commons.exceptions.DataConversionException; +import seedu.budgeteer.commons.util.JsonUtil; +import seedu.budgeteer.model.ReadOnlyUserPrefs; +import seedu.budgeteer.model.UserPrefs; /** * A class to access UserPrefs stored in the hard disk as a json file diff --git a/src/main/java/seedu/budgeteer/storage/PasswordManager.java b/src/main/java/seedu/budgeteer/storage/PasswordManager.java new file mode 100644 index 000000000000..35a5c6d29a79 --- /dev/null +++ b/src/main/java/seedu/budgeteer/storage/PasswordManager.java @@ -0,0 +1,101 @@ +package seedu.budgeteer.storage; + +import static java.util.Objects.requireNonNull; + +import java.io.File; +import java.io.IOException; + +import seedu.budgeteer.commons.exceptions.WrongPasswordException; +import seedu.budgeteer.commons.util.EncryptionUtil; +import seedu.budgeteer.commons.util.FileUtil; +import seedu.budgeteer.logic.PasswordAccepted; +import seedu.budgeteer.logic.PasswordCenter; +import seedu.budgeteer.model.UserPrefs; + + +/** + * Accesses the password file stored on the hard disk + */ +public class PasswordManager { + + /** + * + * @param password user's password + * @throws IOException if file could not be found or created + */ + public static void savePassword(String password) throws IOException { + requireNonNull(password); + File file = new File(getFilePath()); + FileUtil.createIfMissing(file); + FileUtil.writeToFile(file, password); + EncryptionUtil.encrypt(file); + } + + /** + * Check whether to unlock the program + * @param password + * @return + * @throws IOException + */ + public static boolean verifyPassword(String password) throws IOException { + boolean unlock = passwordCheck(password); + if (unlock) { + PasswordCenter.getInstance().post(new PasswordAccepted()); + } + return unlock; + } + + /** + * Removes existing password if user input the correct password + * @param password oldpassword to be checked + * @throws IOException if file does not exists + */ + public static void removePassword(String password) throws IOException, WrongPasswordException { + File file = new File(getFilePath()); + if (passwordCheck(password) && FileUtil.isFileExists(file)) { + file.delete(); + } else { + throw new WrongPasswordException(); + } + } + /** + * Check if password is correct + * @param password to be checked against records + * @return true if password exists, vice-versa + */ + public static boolean passwordCheck(String password) throws IOException { + String storedPassword = getPassword(); + return storedPassword.equals(password); + } + /** + * Check if the password exists + * @return true if password exists, vice-versa + */ + public static boolean passwordExists() { + File file = new File(getFilePath()); + return FileUtil.isFileExists(file); + } + /** + * Method to get the password + * @return password + * @throws IOException if file could not be found + */ + public static String getPassword() throws IOException { + File file = new File(getFilePath()); + EncryptionUtil.decrypt(file); + String password = FileUtil.readFromFile(file); + EncryptionUtil.encrypt(file); + return password; + } + + /** + * Method to get the file path of password + * @return file path + */ + public static String getFilePath() { + UserPrefs userPrefs = new UserPrefs(); + String filePath = userPrefs.getPasswordFilePath(); + + return filePath; + } +} diff --git a/src/main/java/seedu/address/storage/Storage.java b/src/main/java/seedu/budgeteer/storage/Storage.java similarity index 50% rename from src/main/java/seedu/address/storage/Storage.java rename to src/main/java/seedu/budgeteer/storage/Storage.java index beda8bd9f11b..1853bd6c217d 100644 --- a/src/main/java/seedu/address/storage/Storage.java +++ b/src/main/java/seedu/budgeteer/storage/Storage.java @@ -1,18 +1,18 @@ -package seedu.address.storage; +package seedu.budgeteer.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; +import seedu.budgeteer.commons.exceptions.DataConversionException; +import seedu.budgeteer.model.ReadOnlyEntriesBook; +import seedu.budgeteer.model.ReadOnlyUserPrefs; +import seedu.budgeteer.model.UserPrefs; /** * API of the Storage component */ -public interface Storage extends AddressBookStorage, UserPrefsStorage { +public interface Storage extends BudgeteerStorage, UserPrefsStorage { @Override Optional readUserPrefs() throws DataConversionException, IOException; @@ -24,9 +24,9 @@ public interface Storage extends AddressBookStorage, UserPrefsStorage { Path getAddressBookFilePath(); @Override - Optional readAddressBook() throws DataConversionException, IOException; + Optional readEntriesBook() throws DataConversionException, IOException; @Override - void saveAddressBook(ReadOnlyAddressBook addressBook) throws IOException; + void saveAddressBook(ReadOnlyEntriesBook addressBook) throws IOException; } diff --git a/src/main/java/seedu/address/storage/StorageManager.java b/src/main/java/seedu/budgeteer/storage/StorageManager.java similarity index 52% rename from src/main/java/seedu/address/storage/StorageManager.java rename to src/main/java/seedu/budgeteer/storage/StorageManager.java index e4f452b6cbf4..87abeb3171b3 100644 --- a/src/main/java/seedu/address/storage/StorageManager.java +++ b/src/main/java/seedu/budgeteer/storage/StorageManager.java @@ -1,29 +1,29 @@ -package seedu.address.storage; +package seedu.budgeteer.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; +import seedu.budgeteer.commons.core.LogsCenter; +import seedu.budgeteer.commons.exceptions.DataConversionException; +import seedu.budgeteer.model.ReadOnlyEntriesBook; +import seedu.budgeteer.model.ReadOnlyUserPrefs; +import seedu.budgeteer.model.UserPrefs; /** - * Manages storage of AddressBook data in local storage. + * Manages storage of EntriesBook data in local storage. */ public class StorageManager implements Storage { private static final Logger logger = LogsCenter.getLogger(StorageManager.class); - private AddressBookStorage addressBookStorage; + private BudgeteerStorage budgeteerStorage; private UserPrefsStorage userPrefsStorage; - public StorageManager(AddressBookStorage addressBookStorage, UserPrefsStorage userPrefsStorage) { + public StorageManager(BudgeteerStorage budgeteerStorage, UserPrefsStorage userPrefsStorage) { super(); - this.addressBookStorage = addressBookStorage; + this.budgeteerStorage = budgeteerStorage; this.userPrefsStorage = userPrefsStorage; } @@ -45,33 +45,33 @@ public void saveUserPrefs(ReadOnlyUserPrefs userPrefs) throws IOException { } - // ================ AddressBook methods ============================== + // ================ EntriesBook methods ============================== @Override public Path getAddressBookFilePath() { - return addressBookStorage.getAddressBookFilePath(); + return budgeteerStorage.getAddressBookFilePath(); } @Override - public Optional readAddressBook() throws DataConversionException, IOException { - return readAddressBook(addressBookStorage.getAddressBookFilePath()); + public Optional readEntriesBook() throws DataConversionException, IOException { + return readEntriesBook(budgeteerStorage.getAddressBookFilePath()); } @Override - public Optional readAddressBook(Path filePath) throws DataConversionException, IOException { + public Optional readEntriesBook(Path filePath) throws DataConversionException, IOException { logger.fine("Attempting to read data from file: " + filePath); - return addressBookStorage.readAddressBook(filePath); + return budgeteerStorage.readEntriesBook(filePath); } @Override - public void saveAddressBook(ReadOnlyAddressBook addressBook) throws IOException { - saveAddressBook(addressBook, addressBookStorage.getAddressBookFilePath()); + public void saveAddressBook(ReadOnlyEntriesBook addressBook) throws IOException { + saveAddressBook(addressBook, budgeteerStorage.getAddressBookFilePath()); } @Override - public void saveAddressBook(ReadOnlyAddressBook addressBook, Path filePath) throws IOException { + public void saveAddressBook(ReadOnlyEntriesBook addressBook, Path filePath) throws IOException { logger.fine("Attempting to write to data file: " + filePath); - addressBookStorage.saveAddressBook(addressBook, filePath); + budgeteerStorage.saveAddressBook(addressBook, filePath); } } diff --git a/src/main/java/seedu/address/storage/UserPrefsStorage.java b/src/main/java/seedu/budgeteer/storage/UserPrefsStorage.java similarity index 70% rename from src/main/java/seedu/address/storage/UserPrefsStorage.java rename to src/main/java/seedu/budgeteer/storage/UserPrefsStorage.java index 29eef178dbc4..ae5dff0962f4 100644 --- a/src/main/java/seedu/address/storage/UserPrefsStorage.java +++ b/src/main/java/seedu/budgeteer/storage/UserPrefsStorage.java @@ -1,15 +1,15 @@ -package seedu.address.storage; +package seedu.budgeteer.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 seedu.budgeteer.commons.exceptions.DataConversionException; +import seedu.budgeteer.model.ReadOnlyUserPrefs; +import seedu.budgeteer.model.UserPrefs; /** - * Represents a storage for {@link seedu.address.model.UserPrefs}. + * Represents a storage for {@link seedu.budgeteer.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 seedu.budgeteer.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/seedu/address/ui/BrowserPanel.java b/src/main/java/seedu/budgeteer/ui/BrowserPanel.java similarity index 52% rename from src/main/java/seedu/address/ui/BrowserPanel.java rename to src/main/java/seedu/budgeteer/ui/BrowserPanel.java index 53876e01c8d1..72b6473b59d5 100644 --- a/src/main/java/seedu/address/ui/BrowserPanel.java +++ b/src/main/java/seedu/budgeteer/ui/BrowserPanel.java @@ -1,19 +1,19 @@ -package seedu.address.ui; +package seedu.budgeteer.ui; import static java.util.Objects.requireNonNull; import java.net.URL; import java.util.logging.Logger; -import javafx.application.Platform; import javafx.beans.value.ObservableValue; import javafx.event.Event; import javafx.fxml.FXML; +import javafx.scene.control.Label; import javafx.scene.layout.Region; import javafx.scene.web.WebView; -import seedu.address.MainApp; -import seedu.address.commons.core.LogsCenter; -import seedu.address.model.person.Person; +import seedu.budgeteer.MainApp; +import seedu.budgeteer.commons.core.LogsCenter; +import seedu.budgeteer.model.entry.Entry; /** * The Browser Panel of the App. @@ -30,38 +30,54 @@ public class BrowserPanel extends UiPart { @FXML private WebView browser; + @FXML + private Label nameLabel; + @FXML + private Label dateLabel; + @FXML + private Label cashFlowLabel; + @FXML + private Label tagsLabel; - public BrowserPanel(ObservableValue selectedPerson) { + public BrowserPanel(ObservableValue selectedEntry) { super(FXML); // To prevent triggering events for typing inside the loaded Web page. getRoot().setOnKeyPressed(Event::consume); - // Load person page when selected person changes. - selectedPerson.addListener((observable, oldValue, newValue) -> { + // Load entry page when selected entry changes. + selectedEntry.addListener((observable, oldValue, newValue) -> { if (newValue == null) { loadDefaultPage(); return; } - loadPersonPage(newValue); + loadEntryPage(newValue); }); loadDefaultPage(); } - private void loadPersonPage(Person person) { - loadPage(SEARCH_PAGE_URL + person.getName().fullName); + /** + * Loads a page based on given entry + * @param entry + */ + private void loadEntryPage(Entry entry) { + nameLabel.setText("Name: " + entry.getName().fullName); + dateLabel.setText("Date: " + entry.getDate().getValue()); + cashFlowLabel.setText("Cashflow: " + entry.getCashFlow().value); + tagsLabel.setText("Tags: " + entry.getTags().toString()); } - public void loadPage(String url) { - Platform.runLater(() -> browser.getEngine().load(url)); - } + /** * Loads a default HTML file with a background that matches the general theme. */ private void loadDefaultPage() { - loadPage(DEFAULT_PAGE.toExternalForm()); + nameLabel.setText(""); + dateLabel.setText(""); + cashFlowLabel.setText(""); + tagsLabel.setText(""); } } diff --git a/src/main/java/seedu/address/ui/CommandBox.java b/src/main/java/seedu/budgeteer/ui/CommandBox.java similarity index 56% rename from src/main/java/seedu/address/ui/CommandBox.java rename to src/main/java/seedu/budgeteer/ui/CommandBox.java index bf09f3dcbea6..011ec2a49402 100644 --- a/src/main/java/seedu/address/ui/CommandBox.java +++ b/src/main/java/seedu/budgeteer/ui/CommandBox.java @@ -1,15 +1,27 @@ -package seedu.address.ui; +package seedu.budgeteer.ui; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; import javafx.collections.ObservableList; import javafx.fxml.FXML; +import javafx.geometry.Side; +import javafx.scene.control.ContextMenu; +import javafx.scene.control.CustomMenuItem; +import javafx.scene.control.Label; import javafx.scene.control.TextField; import javafx.scene.input.KeyEvent; 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; + +import seedu.budgeteer.commons.core.LogsCenter; +import seedu.budgeteer.logic.commands.CommandResult; +import seedu.budgeteer.logic.commands.exceptions.CommandException; +import seedu.budgeteer.logic.parser.exceptions.ParseException; + /** * The UI component that is responsible for receiving user command inputs. @@ -18,9 +30,23 @@ public class CommandBox extends UiPart { public static final String ERROR_STYLE_CLASS = "error"; private static final String FXML = "CommandBox.fxml"; - + private static final String[] allSuggestions = {"add", "add n/ d/ c/ t/", "clear", "delete", + "filter", "find n/ d/ c/ t/", + "display", "display n/ or d/ or c/ or t/", + "report", "report n/ or s/ or e/ or t/", + "report insight", "report n/ or d/ or c/ or t/", + "find", "lock password", + "find n/ or d/ or c/ or t/", + "edit", "edit n/ d/ c/ t/", "help", + "list", "select", "undo", "redo", + "bitcoin", "ethereum", "litecoin", "stock n/, crypto n/", + "invest interest/ years/"}; + private final Logger logger = LogsCenter.getLogger(CommandBox.class); private final CommandExecutor commandExecutor; private final List history; + private List listedSuggestions = new ArrayList<>(Arrays.asList(allSuggestions)); + private ContextMenu suggestionsMenu = new ContextMenu(); + private ListElementPointer historySnapshot; @FXML @@ -30,9 +56,11 @@ public CommandBox(CommandExecutor commandExecutor, List history) { super(FXML); this.commandExecutor = commandExecutor; this.history = history; + createSuggestions(); // calls #setStyleToDefault() whenever there is a change to the text of the command box. commandTextField.textProperty().addListener((unused1, unused2, unused3) -> setStyleToDefault()); historySnapshot = new ListElementPointer(history); + } /** @@ -52,6 +80,11 @@ private void handleKeyPress(KeyEvent keyEvent) { keyEvent.consume(); navigateToNextInput(); break; + case ESCAPE: + keyEvent.consume(); + suggestionsMenu.hide(); + break; + default: // let JavaFx handle the keypress } @@ -146,9 +179,54 @@ public interface CommandExecutor { /** * Executes the command and returns the result. * - * @see seedu.address.logic.Logic#execute(String) + * @see seedu.budgeteer.logic.Logic#execute(String) */ CommandResult execute(String commandText) throws CommandException, ParseException; } + /** + * Creates a list of Matched Suggestions based on User Input + */ + public void createSuggestions() { + commandTextField.textProperty().addListener((observable, oldValue, newValue) -> { + String userInput = commandTextField.getText(); + List matchedSuggestions = listedSuggestions.stream() + .filter(i-> i.toLowerCase().startsWith(userInput.toLowerCase())) + .collect(Collectors.toList()); + + if (matchedSuggestions.contains(userInput) || userInput.isEmpty() || matchedSuggestions.isEmpty()) { + suggestionsMenu.hide(); + } else { + createPopupWindow(matchedSuggestions); + if (!suggestionsMenu.isShowing()) { + suggestionsMenu.show(this.commandTextField, Side.TOP, 200, 0); // Popup position. + } + } + }); + } + + /** + * Instantiates the Suggestions Popup Window based on the list {@code listedSuggestions}. + * + */ + + private void createPopupWindow(List matchedSuggestions) { + List popupMenu = new ArrayList<>(); + suggestionsMenu.getItems().clear(); + for (int i = 0; i < matchedSuggestions.size(); i++) { + Label suggestion = new Label(matchedSuggestions.get(i)); + suggestion.setPrefHeight(30); + CustomMenuItem item = new CustomMenuItem(suggestion, true); + popupMenu.add(item); + + item.setOnAction(actionEvent -> { + logger.log(Level.INFO, suggestion.getText()); + suggestionsMenu.hide(); + commandTextField.setText(suggestion.getText()); + commandTextField.positionCaret(suggestion.getText().length()); + }); + } + suggestionsMenu.getItems().addAll(popupMenu); + } + } diff --git a/src/main/java/seedu/address/ui/PersonCard.java b/src/main/java/seedu/budgeteer/ui/EntryCard.java similarity index 53% rename from src/main/java/seedu/address/ui/PersonCard.java rename to src/main/java/seedu/budgeteer/ui/EntryCard.java index f6727ea83abd..2341c8576a9f 100644 --- a/src/main/java/seedu/address/ui/PersonCard.java +++ b/src/main/java/seedu/budgeteer/ui/EntryCard.java @@ -1,28 +1,28 @@ -package seedu.address.ui; +package seedu.budgeteer.ui; 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; +import seedu.budgeteer.model.entry.Entry; /** - * An UI component that displays information of a {@code Person}. + * An UI component that displays information of a {@code Entry}. */ -public class PersonCard extends UiPart { +public class EntryCard extends UiPart { - private static final String FXML = "PersonListCard.fxml"; + private static final String FXML = "EntryListCard.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 + * @see The issue on EntriesBook level 4 */ - public final Person person; + public final Entry entry; @FXML private HBox cardPane; @@ -31,23 +31,20 @@ public class PersonCard extends UiPart { @FXML private Label id; @FXML - private Label phone; + private Label date; @FXML - private Label address; - @FXML - private Label email; + private Label cashFlow; @FXML private FlowPane tags; - public PersonCard(Person person, int displayedIndex) { + public EntryCard(Entry entry, int displayedIndex) { super(FXML); - this.person = person; + this.entry = entry; 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().forEach(tag -> tags.getChildren().add(new Label(tag.tagName))); + name.setText(entry.getName().fullName); + date.setText(entry.getDate().value); + cashFlow.setText(entry.getCashFlow().toString()); + entry.getTags().forEach(tag -> tags.getChildren().add(new Label(tag.tagName))); } @Override @@ -58,13 +55,13 @@ public boolean equals(Object other) { } // instanceof handles nulls - if (!(other instanceof PersonCard)) { + if (!(other instanceof EntryCard)) { return false; } // state check - PersonCard card = (PersonCard) other; + EntryCard card = (EntryCard) other; return id.getText().equals(card.id.getText()) - && person.equals(card.person); + && entry.equals(card.entry); } } diff --git a/src/main/java/seedu/budgeteer/ui/EntryListPanel.java b/src/main/java/seedu/budgeteer/ui/EntryListPanel.java new file mode 100644 index 000000000000..777e8f2ab9b6 --- /dev/null +++ b/src/main/java/seedu/budgeteer/ui/EntryListPanel.java @@ -0,0 +1,71 @@ +package seedu.budgeteer.ui; + +import java.util.Objects; +import java.util.function.Consumer; +import java.util.logging.Logger; + +import javafx.beans.value.ObservableValue; +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.budgeteer.commons.core.LogsCenter; +import seedu.budgeteer.model.entry.Entry; + +/** + * Panel containing the list of entrys. + */ +public class EntryListPanel extends UiPart { + private static final String FXML = "EntryListPanel.fxml"; + private final Logger logger = LogsCenter.getLogger(EntryListPanel.class); + + @FXML + private ListView entryListView; + + public EntryListPanel(ObservableList entryList, ObservableValue selectedEntry, + Consumer onSelectedEntryChange) { + super(FXML); + entryListView.setItems(entryList); + entryListView.setCellFactory(listView -> new EntryListViewCell()); + entryListView.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> { + logger.fine("Selection in entry list panel changed to : '" + newValue + "'"); + onSelectedEntryChange.accept(newValue); + }); + selectedEntry.addListener((observable, oldValue, newValue) -> { + logger.fine("Selected entry changed to: " + newValue); + + // Don't modify selection if we are already selecting the selected entry, + // otherwise we would have an infinite loop. + if (Objects.equals(entryListView.getSelectionModel().getSelectedItem(), newValue)) { + return; + } + + if (newValue == null) { + entryListView.getSelectionModel().clearSelection(); + } else { + int index = entryListView.getItems().indexOf(newValue); + entryListView.scrollTo(index); + entryListView.getSelectionModel().clearAndSelect(index); + } + }); + } + + /** + * Custom {@code ListCell} that displays the graphics of a {@code Entry} using a {@code EntryCard}. + */ + class EntryListViewCell extends ListCell { + @Override + protected void updateItem(Entry entry, boolean empty) { + super.updateItem(entry, empty); + + if (empty || entry == null) { + setGraphic(null); + setText(null); + } else { + setGraphic(new EntryCard(entry, getIndex() + 1).getRoot()); + } + } + } + +} diff --git a/src/main/java/seedu/address/ui/HelpWindow.java b/src/main/java/seedu/budgeteer/ui/HelpWindow.java similarity index 89% rename from src/main/java/seedu/address/ui/HelpWindow.java rename to src/main/java/seedu/budgeteer/ui/HelpWindow.java index 22606d102f83..996ad17d2975 100644 --- a/src/main/java/seedu/address/ui/HelpWindow.java +++ b/src/main/java/seedu/budgeteer/ui/HelpWindow.java @@ -1,11 +1,11 @@ -package seedu.address.ui; +package seedu.budgeteer.ui; import java.util.logging.Logger; import javafx.fxml.FXML; import javafx.scene.web.WebView; import javafx.stage.Stage; -import seedu.address.commons.core.LogsCenter; +import seedu.budgeteer.commons.core.LogsCenter; /** * Controller for a help page @@ -28,8 +28,8 @@ public class HelpWindow extends UiPart { public HelpWindow(Stage root) { super(FXML, root); - String userGuideUrl = getClass().getResource(USERGUIDE_FILE_PATH).toString(); - browser.getEngine().load(userGuideUrl); + //String userGuideUrl = getClass().getResource(USERGUIDE_FILE_PATH).toString(); + //browser.getEngine().load(userGuideUrl); } /** diff --git a/src/main/java/seedu/address/ui/ListElementPointer.java b/src/main/java/seedu/budgeteer/ui/ListElementPointer.java similarity index 99% rename from src/main/java/seedu/address/ui/ListElementPointer.java rename to src/main/java/seedu/budgeteer/ui/ListElementPointer.java index 54db5ae7efee..7df1afb8467e 100644 --- a/src/main/java/seedu/address/ui/ListElementPointer.java +++ b/src/main/java/seedu/budgeteer/ui/ListElementPointer.java @@ -1,4 +1,4 @@ -package seedu.address.ui; +package seedu.budgeteer.ui; import java.util.ArrayList; import java.util.List; diff --git a/src/main/java/seedu/address/ui/MainWindow.java b/src/main/java/seedu/budgeteer/ui/MainWindow.java similarity index 80% rename from src/main/java/seedu/address/ui/MainWindow.java rename to src/main/java/seedu/budgeteer/ui/MainWindow.java index ac165736001d..6d2d59c8a430 100644 --- a/src/main/java/seedu/address/ui/MainWindow.java +++ b/src/main/java/seedu/budgeteer/ui/MainWindow.java @@ -1,4 +1,4 @@ -package seedu.address.ui; +package seedu.budgeteer.ui; import java.util.logging.Logger; @@ -10,12 +10,12 @@ 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; +import seedu.budgeteer.commons.core.GuiSettings; +import seedu.budgeteer.commons.core.LogsCenter; +import seedu.budgeteer.logic.Logic; +import seedu.budgeteer.logic.commands.CommandResult; +import seedu.budgeteer.logic.commands.exceptions.CommandException; +import seedu.budgeteer.logic.parser.exceptions.ParseException; /** * The Main Window. Provides the basic application layout containing @@ -32,9 +32,10 @@ public class MainWindow extends UiPart { // Independent Ui parts residing in this Ui container private BrowserPanel browserPanel; - private PersonListPanel personListPanel; + private EntryListPanel entryListPanel; private ResultDisplay resultDisplay; private HelpWindow helpWindow; + private ReportWindow reportWindow = null; @FXML private StackPane browserPlaceholder; @@ -46,7 +47,7 @@ public class MainWindow extends UiPart { private MenuItem helpMenuItem; @FXML - private StackPane personListPanelPlaceholder; + private StackPane entryListPanelPlaceholder; @FXML private StackPane resultDisplayPlaceholder; @@ -67,6 +68,7 @@ public MainWindow(Stage primaryStage, Logic logic) { setAccelerators(); helpWindow = new HelpWindow(); + reportWindow = new ReportWindow(logic); } public Stage getPrimaryStage() { @@ -111,12 +113,12 @@ private void setAccelerator(MenuItem menuItem, KeyCombination keyCombination) { * Fills up all the placeholders of this window. */ void fillInnerParts() { - browserPanel = new BrowserPanel(logic.selectedPersonProperty()); + browserPanel = new BrowserPanel(logic.selectedEntryProperty()); browserPlaceholder.getChildren().add(browserPanel.getRoot()); - personListPanel = new PersonListPanel(logic.getFilteredPersonList(), logic.selectedPersonProperty(), - logic::setSelectedPerson); - personListPanelPlaceholder.getChildren().add(personListPanel.getRoot()); + entryListPanel = new EntryListPanel(logic.getFilteredEntryList(), logic.selectedEntryProperty(), + logic::setSelectedEntry); + entryListPanelPlaceholder.getChildren().add(entryListPanel.getRoot()); resultDisplay = new ResultDisplay(); resultDisplayPlaceholder.getChildren().add(resultDisplay.getRoot()); @@ -152,6 +154,18 @@ public void handleHelp() { } } + /** + * Opens the report window or focuses on it if it's already opened. + */ + @FXML + public void handleReport() { + if (!reportWindow.isShowing()) { + reportWindow.show(); + } else { + reportWindow.focus(); + } + } + void show() { primaryStage.show(); } @@ -165,17 +179,18 @@ private void handleExit() { (int) primaryStage.getX(), (int) primaryStage.getY()); logic.setGuiSettings(guiSettings); helpWindow.hide(); + reportWindow.hide(); primaryStage.hide(); } - public PersonListPanel getPersonListPanel() { - return personListPanel; + public EntryListPanel getEntryListPanel() { + return entryListPanel; } /** * Executes the command and returns the result. * - * @see seedu.address.logic.Logic#execute(String) + * @see seedu.budgeteer.logic.Logic#execute(String) */ private CommandResult executeCommand(String commandText) throws CommandException, ParseException { try { @@ -187,6 +202,10 @@ private CommandResult executeCommand(String commandText) throws CommandException handleHelp(); } + if (commandResult.isShowReport()) { + handleReport(); + } + if (commandResult.isExit()) { handleExit(); } diff --git a/src/main/java/seedu/budgeteer/ui/ReportWindow.java b/src/main/java/seedu/budgeteer/ui/ReportWindow.java new file mode 100644 index 000000000000..b0a2e17f63e9 --- /dev/null +++ b/src/main/java/seedu/budgeteer/ui/ReportWindow.java @@ -0,0 +1,232 @@ +package seedu.budgeteer.ui; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.logging.Logger; + +import javafx.beans.binding.Bindings; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.chart.PieChart; +import javafx.scene.control.Label; +import javafx.stage.Stage; +import seedu.budgeteer.commons.core.LogsCenter; +import seedu.budgeteer.logic.Logic; +import seedu.budgeteer.logic.parser.ReportCommandParser; +import seedu.budgeteer.model.entry.Entry; +import seedu.budgeteer.model.entry.ReportEntryList; + + + +/** + * Controller for a help page + */ +public class ReportWindow extends UiPart { + + + private static final Logger logger = LogsCenter.getLogger(ReportWindow.class); + private static final String FXML = "ReportWindow.fxml"; + + private Logic logic; + + private Boolean isDetailsHidden = false; + + @FXML + private PieChart pieChart; + @FXML + private PieChart expenseInsightPieChart; + @FXML + private PieChart incomeInsightPieChart; + @FXML + private Label tLabel; + @FXML + private Label eLabel; + @FXML + private Label iLabel; + @FXML + private Label bitcoinLabel; + @FXML + private Label incomeBreakdownLabel; + @FXML + private Label expenseBreakdownLabel; + + /** + * Creates a new HelpWindow. + * + * @param root Stage to use as the root of the HelpWindow. + */ + public ReportWindow(Stage root, Logic logic) { + super(FXML, root); + this.logic = logic; + + refresh(); + } + + /** + * Creates a new HelpWindow. + */ + public ReportWindow(Logic logic) { + this(new Stage(), logic); + } + + /** + * Refreshes the display with updated filtered entry list. + */ + private void refresh() { + ObservableList filteredReportList = logic.getFilteredEntryList(); + ReportEntryList reportEntryList = new ReportEntryList(filteredReportList); + + Double total = reportEntryList.getTotal(); + Double income = reportEntryList.getTotalIncome(); + Double expense = reportEntryList.getTotalExpense(); + + ObservableList pieChartData = getExpenseIncomePieChartData(reportEntryList); + ObservableList expenseInsightPieChartData = getExpenseInsightPieChartData(reportEntryList); + ObservableList incomeInsightPieChartData = getIncomeInsightPieChartData(reportEntryList); + + pieChart.setData(pieChartData); + expenseInsightPieChart.setData(expenseInsightPieChartData); + incomeInsightPieChart.setData(incomeInsightPieChartData); + tLabel.setText("Total (Income - Expenses): " + String.format("%.02f", total)); + iLabel.setText("Total Income: " + String.format("%.02f", income)); + eLabel.setText("Total Expense: " + String.format("%.02f", expense)); + + if (!ReportCommandParser.isRequireDetailedReport() && !isDetailsHidden) { + expenseInsightPieChart.setVisible(false); + incomeInsightPieChart.setVisible(false); + expenseBreakdownLabel.setVisible(false); + incomeBreakdownLabel.setVisible(false); + expenseInsightPieChart.setManaged(false); + incomeInsightPieChart.setManaged(false); + expenseBreakdownLabel.setManaged(false); + incomeBreakdownLabel.setManaged(false); + isDetailsHidden = true; + } else if (ReportCommandParser.isRequireDetailedReport() && isDetailsHidden) { + expenseInsightPieChart.setVisible(true); + incomeInsightPieChart.setVisible(true); + expenseBreakdownLabel.setVisible(true); + incomeBreakdownLabel.setVisible(true); + expenseInsightPieChart.setManaged(true); + incomeInsightPieChart.setManaged(true); + expenseBreakdownLabel.setManaged(true); + incomeBreakdownLabel.setManaged(true); + isDetailsHidden = false; + } + + } + + /** + * 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."); + refresh(); + getRoot().show(); + } + + /** + * 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() { + refresh(); + getRoot().requestFocus(); + } + + + + private ObservableList getExpenseIncomePieChartData(ReportEntryList reportEntryList) { + Double income = reportEntryList.getTotalIncome(); + Double expense = reportEntryList.getTotalExpense(); + ObservableList pieChartData = FXCollections.observableArrayList( + new PieChart.Data("Income", income), + new PieChart.Data("Expense", expense) + ); + + pieChartData.forEach(data -> + data.nameProperty().bind( + Bindings.concat( + data.getName(), " $", data.pieValueProperty() + ) + ) + ); + + return pieChartData; + } + + private ObservableList getIncomeInsightPieChartData(ReportEntryList reportEntryList) { + ArrayList pieChartDataArr = new ArrayList<>(); + HashMap incomeInsight = reportEntryList.getIncomeCompositionMap(); + Iterator it = incomeInsight.entrySet().iterator(); + while (it.hasNext()) { + HashMap.Entry pair = (HashMap.Entry) it.next(); + PieChart.Data pieData = new PieChart.Data((String) pair.getKey(), (Double) pair.getValue()); + pieChartDataArr.add(pieData); + it.remove(); // avoids a ConcurrentModificationException + } + ObservableList pieChartData = FXCollections.observableArrayList(pieChartDataArr); + pieChartData.forEach(data -> + data.nameProperty().bind( + Bindings.concat( + data.getName(), " $", data.pieValueProperty() + ) + ) + ); + + return pieChartData; + + } + + private ObservableList getExpenseInsightPieChartData(ReportEntryList reportEntryList) { + ArrayList pieChartDataArr = new ArrayList<>(); + HashMap expenseInsight = reportEntryList.getExpenseCompositionMap(); + Iterator it = expenseInsight.entrySet().iterator(); + while (it.hasNext()) { + HashMap.Entry pair = (HashMap.Entry) it.next(); + PieChart.Data pieData = new PieChart.Data((String) pair.getKey(), (Double) pair.getValue()); + pieChartDataArr.add(pieData); + it.remove(); // avoids a ConcurrentModificationException + } + ObservableList pieChartData = FXCollections.observableArrayList(pieChartDataArr); + pieChartData.forEach(data -> + data.nameProperty().bind( + Bindings.concat( + data.getName(), " $", data.pieValueProperty() + ) + ) + ); + + return pieChartData; + + } +} diff --git a/src/main/java/seedu/address/ui/ResultDisplay.java b/src/main/java/seedu/budgeteer/ui/ResultDisplay.java similarity index 95% rename from src/main/java/seedu/address/ui/ResultDisplay.java rename to src/main/java/seedu/budgeteer/ui/ResultDisplay.java index 7d98e84eedf0..59287bdfbc50 100644 --- a/src/main/java/seedu/address/ui/ResultDisplay.java +++ b/src/main/java/seedu/budgeteer/ui/ResultDisplay.java @@ -1,4 +1,4 @@ -package seedu.address.ui; +package seedu.budgeteer.ui; import static java.util.Objects.requireNonNull; diff --git a/src/main/java/seedu/address/ui/StatusBarFooter.java b/src/main/java/seedu/budgeteer/ui/StatusBarFooter.java similarity index 92% rename from src/main/java/seedu/address/ui/StatusBarFooter.java rename to src/main/java/seedu/budgeteer/ui/StatusBarFooter.java index b22e1f525256..cce86518819a 100644 --- a/src/main/java/seedu/address/ui/StatusBarFooter.java +++ b/src/main/java/seedu/budgeteer/ui/StatusBarFooter.java @@ -1,4 +1,4 @@ -package seedu.address.ui; +package seedu.budgeteer.ui; import java.nio.file.Path; import java.nio.file.Paths; @@ -8,7 +8,7 @@ import javafx.fxml.FXML; import javafx.scene.control.Label; import javafx.scene.layout.Region; -import seedu.address.model.ReadOnlyAddressBook; +import seedu.budgeteer.model.ReadOnlyEntriesBook; /** * A ui for the status bar that is displayed at the footer of the application. @@ -36,7 +36,7 @@ public class StatusBarFooter extends UiPart { private Label saveLocationStatus; - public StatusBarFooter(Path saveLocation, ReadOnlyAddressBook addressBook) { + public StatusBarFooter(Path saveLocation, ReadOnlyEntriesBook addressBook) { super(FXML); addressBook.addListener(observable -> updateSyncStatus()); syncStatus.setText(SYNC_STATUS_INITIAL); diff --git a/src/main/java/seedu/budgeteer/ui/SummaryEntry.java b/src/main/java/seedu/budgeteer/ui/SummaryEntry.java new file mode 100755 index 000000000000..f4fe9242dea8 --- /dev/null +++ b/src/main/java/seedu/budgeteer/ui/SummaryEntry.java @@ -0,0 +1,72 @@ +package seedu.budgeteer.ui; + +import javafx.beans.property.SimpleStringProperty; +import seedu.budgeteer.model.summary.Summary; + +/** + * This represents a UI friendly summary entry and methods to convert a summary into a summary entry. + */ +public class SummaryEntry { + + private final SimpleStringProperty identifier; + private final SimpleStringProperty totalIncome; + private final SimpleStringProperty totalExpense; + private final SimpleStringProperty total; + + public SummaryEntry(String timeStamp, String totalIncome, String totalExpense, String total) { + this.identifier = new SimpleStringProperty(timeStamp); + this.totalIncome = new SimpleStringProperty(totalIncome); + this.totalExpense = new SimpleStringProperty(totalExpense); + this.total = new SimpleStringProperty(total); + } + + /** + * Converts each {@code Summary} to a UI friendly counterpart for display + */ + public static SummaryEntry convertToUiFriendly(Summary summary) { + return new SummaryEntry(summary.getIdentifier().toString(), summary.getTotalIncome().toString(), + summary.getTotalExpense().toString(), summary.getTotal().toString()); + } + + public String getIdentifier() { + return identifier.get(); + } + + public String getTotalIncome() { + return totalIncome.get(); + } + + public void setTotalIncome(String totalIncome) { + this.totalIncome.set(totalIncome); + } + + public String getTotalExpense() { + return totalExpense.get(); + } + + public void setTotalExpense(String totalExpense) { + this.totalExpense.set(totalExpense); + } + + public String getTotal() { + return total.get(); + } + + public void setTotal(String total) { + this.total.set(total); + } + + public void setIdentifier(String identifier) { + this.identifier.set(identifier); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof SummaryEntry // instanceof handles nulls + && identifier.toString().equals(((SummaryEntry) other).identifier.toString()) + && totalIncome.toString().equals(((SummaryEntry) other).totalIncome.toString()) + && totalExpense.toString().equals(((SummaryEntry) other).totalExpense.toString()) + && total.toString().equals(((SummaryEntry) other).total.toString())); + } +} diff --git a/src/main/java/seedu/address/ui/Ui.java b/src/main/java/seedu/budgeteer/ui/Ui.java similarity index 85% rename from src/main/java/seedu/address/ui/Ui.java rename to src/main/java/seedu/budgeteer/ui/Ui.java index 17aa0b494fe3..8007c7f8e35c 100644 --- a/src/main/java/seedu/address/ui/Ui.java +++ b/src/main/java/seedu/budgeteer/ui/Ui.java @@ -1,4 +1,4 @@ -package seedu.address.ui; +package seedu.budgeteer.ui; import javafx.stage.Stage; diff --git a/src/main/java/seedu/address/ui/UiManager.java b/src/main/java/seedu/budgeteer/ui/UiManager.java similarity index 93% rename from src/main/java/seedu/address/ui/UiManager.java rename to src/main/java/seedu/budgeteer/ui/UiManager.java index 876621d79b94..06c7e680490d 100644 --- a/src/main/java/seedu/address/ui/UiManager.java +++ b/src/main/java/seedu/budgeteer/ui/UiManager.java @@ -1,4 +1,4 @@ -package seedu.address.ui; +package seedu.budgeteer.ui; import java.util.logging.Logger; @@ -7,10 +7,10 @@ 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; +import seedu.budgeteer.MainApp; +import seedu.budgeteer.commons.core.LogsCenter; +import seedu.budgeteer.commons.util.StringUtil; +import seedu.budgeteer.logic.Logic; /** * The manager of the UI component. diff --git a/src/main/java/seedu/address/ui/UiPart.java b/src/main/java/seedu/budgeteer/ui/UiPart.java similarity index 97% rename from src/main/java/seedu/address/ui/UiPart.java rename to src/main/java/seedu/budgeteer/ui/UiPart.java index fc820e01a9c3..3dec516d8617 100644 --- a/src/main/java/seedu/address/ui/UiPart.java +++ b/src/main/java/seedu/budgeteer/ui/UiPart.java @@ -1,4 +1,4 @@ -package seedu.address.ui; +package seedu.budgeteer.ui; import static java.util.Objects.requireNonNull; @@ -6,7 +6,7 @@ import java.net.URL; import javafx.fxml.FXMLLoader; -import seedu.address.MainApp; +import seedu.budgeteer.MainApp; /** * Represents a distinct part of the UI. e.g. Windows, dialogs, panels, status bars, etc. diff --git a/src/main/resources/view/BrowserPanel.fxml b/src/main/resources/view/BrowserPanel.fxml index 31670827e3da..8ba2c589544e 100644 --- a/src/main/resources/view/BrowserPanel.fxml +++ b/src/main/resources/view/BrowserPanel.fxml @@ -1,8 +1,11 @@ - + - + diff --git a/src/main/resources/view/DarkTheme.css b/src/main/resources/view/DarkTheme.css index 36e6b001cd8d..878974e4f0dd 100644 --- a/src/main/resources/view/DarkTheme.css +++ b/src/main/resources/view/DarkTheme.css @@ -25,7 +25,7 @@ } .text-field { - -fx-font-size: 12pt; + -fx-font-size: 15pt; -fx-font-family: "Segoe UI Semibold"; } @@ -123,7 +123,7 @@ .cell_big_label { -fx-font-family: "Segoe UI Semibold"; -fx-font-size: 16px; - -fx-text-fill: #010504; + -fx-text-fill: #FFFF66; } .cell_small_label { diff --git a/src/main/resources/view/PersonListCard.fxml b/src/main/resources/view/EntryListCard.fxml similarity index 84% rename from src/main/resources/view/PersonListCard.fxml rename to src/main/resources/view/EntryListCard.fxml index f08ea32ad558..304f965f1ead 100644 --- a/src/main/resources/view/PersonListCard.fxml +++ b/src/main/resources/view/EntryListCard.fxml @@ -28,9 +28,8 @@