<repo_name>CS2103-AY1819S1-T16-1/main<file_sep>/docs/UserGuide.adoc
= WishBook - User Guide
:site-section: UserGuide
:toc:
:toc-title:
:toc-placement: preamble
:sectnums:
:imagesDir: images
:stylesDir: stylesheets
:xrefstyle: full
:experimental:
ifdef::env-github[]
:tip-caption: :bulb:
:note-caption: :information_source:
endif::[]
:repoURL: https://github.com/CS2103-AY1819S1-T16-1/main

By: `Team SE-EDU`      Since: `Jun 2016`      Licence: `MIT`

== Introduction
**WishBook** (WB) is a piggy bank in a digital app form, made for people who need a user-friendly solution for keeping
 track of their disposable income. Not only does WB keep records of your savings, it also allows you to set items
  as goals for users to work towards, serving as a way to cultivate the habit of saving. WB is made for people who have
   material wants in life, but are uncertain of purchasing said wants, by helping them to reserve disposable income so
    that they know they can spend on their wants. So what are you waiting for? Go to Section 2, <<Quick Start>> and
     start saving!

== Quick Start

.  Ensure you have Java version `9` or later installed in your Computer.
.  Download the latest `wishbook.jar` link:{repoURL}/releases[here].
.  Copy the file to the folder you want to use as the home folder for your WishBook app.
.  Double-click the file to start the app. The GUI should appear in a few seconds.
+
image::Ui.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 the items you have set as wishes (sorted by due date).
* **`add`**`n/uPhoneXX p/1000 a/5m` : adds an item “uPhoneXX” as a goal to be completed in 5 months.
* *`help`*: displays list of command with usage.
* *`clear`*: clears view
* *`exit`* : exits the app


.  Refer to <<Features>> for details of each command.

[[Features]]
== Features

====
*Command Format*

* Words in `UPPER_CASE` are the parameters to be supplied by the user e.g. in `add WISH`, `WISH` is a parameter which
can be used as add iPhone.
* Items in square brackets are optional e.g. in `list [FILTER]`, `FILTER` is an optional parameter, since the list
command can be used as `list` to display all wishes in WishBook.
** An exception to this is `TIME_GIVEN` and `END_DATE`, whereby only one of the two can be used in any command.
** The `add` command requires either `TIME_GIVEN` and `END_DATE`.
** The `edit` command can take either or none of them.
* 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/broke`, `t/needs` etc.
* The `/` symbol between parameters means that you can use either of the parameters types in the command e.g. in
`add WISH PRICE [TIME_GIVEN]/[START_DATE to END_DATE]`, you provide either the `TIME_GIVEN` parameter or `START_DATE`
and `END_DATE` parameters.

====

=== Viewing help : `help`
Displays a popup window showing all the commands a user can use in `WishBook`. +
Format: `help`

=== Add a new wish: `add`
Add a new wish to the wish list. +
Format: `add n/WISH_NAME p/PRICE [a/PERIOD_GIVEN]/[d/END_DATE] [u/URL] [t/TAG]...`

[TIP]
* `[END_DATE]`: Specified in _dd/mm/yyyy_ format.
* `[TIME_GIVEN]`: Specified in terms of years, months, weeks, and days, suffixes (coming after the value)
marking such time periods are _‘y’_, _‘m’_, _‘w’_, and _‘d’_ respectively.

The order of `[TIME_GIVEN]` must be from the biggest unit of time to the smallest unit of time, meaning that the
suffix _`y`_ cannot come after any of the other three suffixes, and _'w'_ cannot come after _'d'_, but can come
after _'y'_ and _'m'_.

[NOTE]
====
If you enter an invalid date, a warning message will be displayed to prompt the user to reenter a valid date.
Until all fields provided are valid, the wish will not be added to `WishBook`.
====

[NOTE]
====
The expiry date you enter must be after current date.
====

Examples:

* `add n/XBoxTwo p/999 a/1y`
* `add n/kfcBook 13inch p/2300 a/6m3w r/For dad t/family t/computing`
* `add n/prinkles p/1.95 d/24/04/2020`
* `add n/prinkles p/1.95 d/24/04/2020 u/www.amazon.com/prinkles t/high`

=== Edit a wish : `edit`

Edits an existing wish in the wish list. +
Format: `edit INDEX [n/WISH_NAME] [p/PRICE] [a/TIME_GIVEN]/[d/END_DATE] [u/URL] [t/TAG]`

****
* Edits the wish at the specified `INDEX`. `INDEX` refers to the index number shown in the displayed list of goals. `INDEX` must be a positive integer 1, 2, 3, …
* `INDEX` is labelled at the side of each wish.
* 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 wish will be removed i.e. adding of tags is not cumulative.
* You can remove all tags by typing `t/` without specifying any tags after it.
****

Examples:

* `edit 1 n/Macbook Pro t/Broke wishes` +
Edits the name of the wish and the tag of the 1st wish to be Macbook Pro and Broke wishes respectively

* `edit 2 p/22 a/22w` +
Edits the price and time given to accomplish the 2nd wish to 22 (in the chosen currency) and 22 weeks respectively.

=== List wishes : `list`

Shows a list of all the wishes you have set, sorted by date by default, based on the given filter.
If no filter is specified, all wishes in the WishBook will be listed. +
Format: `list [FILTER]`

* `list` +
Lists all the wishes in the WishBook.

* `list -c` +
Lists all the completed wishes in the WishBook.

* `list -u` +
Lists all the uncompleted wishes in the WishBook.

****
* Only wishes in the current state of the wishbook will be listed.
* Deleted wishes will not be displayed.
****

=== List history of entered commands : `history -c`

Lists all the commands that you have entered in reverse chronological order. +
Format: `history -c`

[NOTE]
====
Pressing the kbd:[&uarr;] and kbd:[&darr;] arrows will display the previous and next input respectively in the command
box.
====

// tag::savingsHistory[]
=== View your saving history: `history -s`

Shows a history of savings you have allocated, from newest to oldest. +
Format: `history -s`

[NOTE]
====
Only history of wishes which currently exist in the `WishBook` will be stored. (i.e. wishes that have been deleted will
no longer be tracked.)
====
// end::savingsHistory[]

// tag::find[]
=== Locate your wish with speed and precision: `find`

Find wishes which match the given search keywords. +
Format: `find [-e] [n/NAME_KEYWORD]... [t/TAG_KEYWORD]... [r/REMARK_KEYWORD]...`

****
* At least one keyword must be provided.
* Searching multiple keywords of the *same prefix* will return wishes whose
attribute corresponding to the prefix contain *any* one of the keywords.
* Searching with keywords of *different prefixes* will return only wishes that
match will *all* the keywords of the different prefixes.
* Using the exact match flag, `-e` returns wishes whose corresponding attributes contain *all*
the keywords.
* The search is case insensitive. e.g. watch will match Watch.
****

Examples:

* `find n/wat` +
Returns any wish whose name contains the _wat_.

* `find n/wat n/balloon n/appl` +
Returns wishes whose names which contain at least any one of _wat_, _balloon_ or _appl_.

* `find -e n/wat n/balloon n/appl` +
Returns only wishes whose names that contain all of _wat_, _balloon_ and _appl_.

* `find n/watch t/important` +
Returns any wish whose name contains _watch_, and whose tags contains _broke wishes_.

* `find n/wat n/balloon t/important` +
Returns any wish whose name contains _wat_ or _balloon_, and whose tags contains
_important_.
// end::find[]

=== Delete a wish : `delete`

Deletes the specified wish from the list and moves any funds in that wish to `unusedFunds`. +
Format: `delete INDEX`

****
* `INDEX` refers to the index number shown in the displayed list.
* `INDEX` must be a positive integer 1, 2, 3...
* If the wish at `INDEX` is not yet fulfilled, the saved amount in that wish will be channelled to `unusedFunds`.
****

Examples:

* `list` +
`delete 2` +
Deletes the 2nd wish in the list.

* `find watch` +
`delete 1` +
Deletes the 1st wish in the results of the find command (if any).

=== Select a wish : `select`

Selects the wish identified by the index number used in the displayed wish list. +
Format: `select INDEX`

****
* Selects the wish and displays content relevant to that wish in the view.
* Webpage at the `url` specific to that wish at the specified `INDEX` will be loaded in the view.
* If there is no internet connection, the webpage will not be loaded.
* Savings history view will also reflect the savings history for the selected wish, if the selected wish is valid.
* `INDEX` refers to the index number shown in the displayed wish list.
* `INDEX` *must be a positive integer* `1, 2, 3, ...`
****

Examples:

* `list` +
`select 2` +
Selects the 2nd wish in the wish list.

* `find price` +
`select 1` +
Selects the 1st wish in the results of the `find` command.

// tag::save[]
=== Save money for a particular wish: `save`

Channel a specified amount of money to savings for a specified wish. +
Format: `save INDEX AMOUNT`

****
* `INDEX` should be a positive integer 1, 2, 3… no larger than the number of wishes.
* If `INDEX` is 0, `AMOUNT` will be channelled directly to `unusedFunds`.
* If `AMOUNT` saved to `INDEX` is greater than the amount needed to fulfil that wish, excess funds will be channelled
to `unusedFunds`.
* If `AMOUNT` is negative, money will be removed from amount saved for that wish. +
* `AMOUNT` will not be accepted if:
** `AMOUNT` brings the savings value for that wish to below 0.
** The wish at `INDEX` is already fulfilled.
****

Examples: +

* `save 1 1000` +
Attempt to save $1000 for the wish at index 1.

* `save 1 -100.50` +
Attempt to remove $100.50 from the savings for the wish at index 1.

* `save 0 100.50` +
Attempt save $100.50 to `unusedFunds`.

// end::save[]

// tag::move[]
=== Move money between wishes: `move`

Moves funds from one wish to another. +
Format: `move FROM_WISH_INDEX TO_WISH_INDEX AMOUNT`

****
* `FROM_WISH_INDEX` and `TO_WISH_INDEX` should be a positive integer 1, 2, 3… no larger than the number of wishes
* If `FROM_WISH_INDEX` is 0, `AMOUNT` will be channelled from `unusedFunds` to `TO_WISH_INDEX`.
* If `TO_WISH_INDEX` is 0, `AMOUNT` will be channelled from `FROM_WISH_INDEX` to `unusedFunds`.
* `AMOUNT` from `unusedFunds` will only be successfully channelled if the exact amount requested is present in `unusedFunds`.
* If `FROM_WISH_INDEX` equals `TO_WISH_INDEX`, both indexes will not be accepted.
* If `AMOUNT` saved to `TO_WISH_INDEX` is greater than the amount needed to fulfil that wish, excess funds will be channelled to `unusedFunds`.
* `AMOUNT` will not be accepted if:
** `AMOUNT` is negative.
** `AMOUNT` brings the savings amount of wish at `FROM_WISH_INDEX` to below 0.
** Either wish at `FROM_WISH_INDEX` or `TO_WISH_INDEX` is already fulfilled.
****

[NOTE]
====
Index 0 is specially allocated for `unused funds`. Excess funds when user attempts to save to a wish will be
automatically allocated to `unusedFunds`.
The user can also choose to channel funds from `unusedFunds` to a valid wish.
====

Examples: +

* `move 1 2 10` +
Attempt to move $10 from the wish at index 1 to the wish at index 2.

* `move 0 1 10` +
Attempt to move $10 from `unusedFunds` to the wish at index 1.

* `move 1 0 10` +
Attempt to move $10 from the wish at index 1 to `unusedFunds`.

// end::move[]

=== Edit a remark for a wish : `remark`

Edits the remark for a wish specified in the index. +
Format: `remark INDEX r/[REMARK]`

****
* `INDEX` refers to the index number shown in the displayed list.
* `INDEX` must be a positive integer 1, 2, 3...
****

Examples:

* `list` +
`remark 1 r/Buying this for dad.` +
Edits the remark for the first wish to `Buying this for dad.`

* `list` +
`remark 1 r/` +
Removes the remark for the first wish (if any).

// tag::undoredo[]
=== Undo previous command : `undo`

Restores WishBook to the state before the previous undoable command was executed. +
Format: `undo`

****
* If no undoable commands exist or the state of the wishbook is already in its original state, the undo command will
fail and the state of the wishbook will remain unchanged.
****

[NOTE]
====
Undoable commands: commands that modify WishBook content (`add, delete, edit, save`).
====

Examples:

* `delete 1` +
`list` +
`undo` (reverses the `delete 1` command) +

* `select 1` +
`list` +
`undo` +
The `undo` command fails as there are no undoable commands executed previously.

* `delete 1` +
`clear` +
`undo` (reverses the `clear` command) +
`undo` (reverses the `delete 1` command) +

=== Redo the previously undone command : `redo`

Reverses the most recent `undo` command. +
Format: `redo`

Examples:

* `delete 1` +
`undo` (reverses the `delete 1` command) +
`redo` (reapplies the `delete 1` command) +

* `delete 1` +
`redo` +
The `redo` command fails as there are no `undo` commands executed previously.

* `delete 1` +
`clear` +
`undo` (reverses the `clear` command) +
`undo` (reverses the `delete 1` command) +
`redo` (reapplies the `delete 1` command) +
`redo` (reapplies the `clear` command) +
// end::undoredo[]

=== Clearing all entries : `clear`

Clears all entries from WishBook. +
Format: `clear`

=== Exiting the program : `exit`

Exits the program. +
Format: `exit`

=== Saving the data

WishBook data are saved in the hard disk automatically after any command that changes the data. +
There is no need to save manually.

== 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 WishBook folder.

== Command Summary

* *Add*  `add n/WISH_NAME p/PRICE t/[a/TIME_GIVEN]/[d/END_DATE] [u/URL] [t/TAG]...` +
e.g. `add n/Sega Genesis p/2300 a/6m3w`
* *Clear* : `clear`
* *Delete* : `delete INDEX` +
e.g. `delete 3`
* *Edit* : `edit INDEX [n/WISH_NAME] [p/PRICE] [a/TIME_GIVEN]/[d/END_DATE] [t/TAG]` +
e.g. `edit 1 n/Macbook Pro t/technology`
* *Find* : `find [-e] [n/NAME_KEYWORD]... [t/TAG_KEYWORD]... [r/REMARK_KEYWORD]...` +
e.g. `find n/Vacuum Cleaner t/family`
* *List* : `list`
* *List completed* : `list -c`
* *List uncompleted* : `list -u`
* *Help* : `help`
* *Select* : `select INDEX` +
e.g.`select 2`
* *Save* : `save INDEX AMOUNT` +
e.g. `save 1 1000`
* *Move* : `move FROM_WISH_INDEX TO_WISH_INDEX AMOUNT` +
e.g. `move 1 2 10`
* *Command History* : `history -c`
* *Savings History* : `history -s`
* *Remark* : `remark INDEX r/[REMARK]`
* *Undo* : `undo`
* *Redo* : `redo`
* *Exit* : `exit`
<file_sep>/src/main/java/seedu/address/model/wish/WishContainsKeywordsPredicate.java
package seedu.address.model.wish;

import java.util.List;
import java.util.function.Predicate;

/**
 * Tests that a {@code Wish}'s {@code Name}, {@code Tags} and {@code Remark} match any or all of the keywords given.
 */
public class WishContainsKeywordsPredicate implements Predicate<Wish> {
    private final boolean isExactMatch;
    private final List<String> nameKeywords;
    private final List<String> tagKeywords;
    private final List<String> remarkKeywords;

    /**
     * Constructs a {@link WishContainsKeywordsPredicate} using the specified keywords.
     * @param nameKeywords Keywords to test against {@code Wish} name.
     * @param tagKeywords Keywords to test against {@code Wish}  tags.
     * @param remarkKeywords Keywords to test against {@code Wish} remark.
     * @param isExactMatch Indicate whether to test for a match with a single keyword or a match with all the keywords
     *                     for each {@code Wish} field.
     */
    public WishContainsKeywordsPredicate(List<String> nameKeywords, List<String> tagKeywords,
                                         List<String> remarkKeywords, boolean isExactMatch) {
        this.isExactMatch = isExactMatch;
        this.nameKeywords = nameKeywords;
        this.tagKeywords = tagKeywords;
        this.remarkKeywords = remarkKeywords;
    }

    @Override
    public boolean test(Wish wish) {
        if (isExactMatch) {
            return testAllMatch(wish.getName().toString(), nameKeywords)
                    && testAllMatch(wish.getTags().toString(), tagKeywords)
                    && testAllMatch(wish.getRemark().toString(), remarkKeywords);
        }
        return testAnyMatch(wish.getName().toString(), nameKeywords)
                && testAnyMatch(wish.getTags().toString(), tagKeywords)
                && testAnyMatch(wish.getRemark().toString(), remarkKeywords);
    }

    @Override
    public boolean equals(Object other) {
        return other == this //short circuit if same object
                || ((other instanceof WishContainsKeywordsPredicate)
                && isExactMatch == ((WishContainsKeywordsPredicate) other).isExactMatch
                && nameKeywords.equals(((WishContainsKeywordsPredicate) other).nameKeywords)
                && tagKeywords.equals(((WishContainsKeywordsPredicate) other).tagKeywords)
                && remarkKeywords.equals(((WishContainsKeywordsPredicate) other).remarkKeywords));
    }

    /** Returns true if all keyword is found in the sentence, case insensitive. */
    private boolean testAllMatch(String sentence, List<String> keywords) {
        String processedSentence = sentence.toLowerCase();
        return keywords.stream().allMatch(keyword -> processedSentence.contains(keyword.toLowerCase()));
    }

    /** Returns true if any keyword is found in the sentence, case insensitive. */
    private boolean testAnyMatch(String sentence, List<String> keywords) {
        if (keywords.isEmpty()) {
            return true;
        }
        String processedSentence = sentence.toLowerCase();
        return keywords.stream().anyMatch(keyword -> processedSentence.contains(keyword.toLowerCase()));
    }
}
<file_sep>/docs/team/jiholim.adoc
= <NAME> (jiholim) - Project Portfolio
:site-section: AboutUs
:imagesDir: ../images
:stylesDir: ../stylesheets
ifdef::env-github[]
:tip-caption: :bulb:
:note-caption: :information_source:
:warning-caption: :warning:
:experimental:
endif::[]

== PROJECT: WishBook

---

== Overview

WishBook is a desktop saving assistant application built for people who want to set saving goals and keep track of their progress.
WishBook makes saving money easy, motivating, & fun and aims to help users to complete their saving goals by making sure they stick to the target and maintain progress regularly.
WishBook boasts a beautifully designed Graphical User Interface (GUI) and makes use of a Command Line Interface (CLI) for most of the user interaction. The application is written in Java and contains about 10 kLoC.

Below is a high definition product preview photo.

image::Ui2.png[width="800"]

== Summary of contributions

* *Major enhancement*: A complete redesign of GUI (UI & UX) and front-end development
** What it does: Displays an intuitive and data-rich graphical user interface that achieves the product’s goal and
delivers smooth user experience when the features are being used.
** Justification: The redesign of the application is necessary as it unifies the different set of features that are built into a well-integrated product.
It improves the product significantly because user can reap the maximum intended benefits of the features. 
** Highlights: This enhancement affects existing ui components and elements 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 ui-related codebase

* *Minor enhancements*:
** Devised and built appropriate event handlers to present changes in model on ui
** Various Bug Fixes
** Update HelpWindow display

* *Code contributed*: https://nus-cs2103-ay1819s1.github.io/cs2103-dashboard/#=undefined&search=jiholim[Project Code Dashboard link]

* *Other contributions*:

** Project management:
*** Managed milestones `v1.1` - `v1.4` and one `v1.5` (release) on GitHub
*** Helped with the issue tracking using labels and assignees on Github

** Documentation:
*** Updated README file to add descriptions of the application with relevant mockup picture (Pull requests https://github.com/CS2103-AY1819S1-T16-1/main/pull/7[#7])
*** Updated User Guide and Developer Guide for the features I added.

** Community:
*** Review pull requests weekly (https://github.com/CS2103-AY1819S1-T16-1/main/pulls?q=is%3Apr+is%3Aclosed[see here])
*** Reported bugs and gave suggestions for other group’s project.

== 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._
|===

=== List wishes : `list`

Shows a list of all the wishes you have set, sorted by date by default, based on the given filter.
If no filter is specified, all wishes in the WishBook will be listed. +
Format: `list [FILTER]`

* `list` +
Lists all the wishes in the WishBook.

* `list -c` +
Lists all the completed wishes in the WishBook.

* `list -u` +
Lists all the uncompleted wishes in the WishBook.

****
* Only wishes in the current state of the wishbook will be listed.
* Deleted wishes will not be displayed.
****

== 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._
|===

=== List feature

The `list -c and list -u` command allows the user to view the list of all wishes, completed and ongoing, respectively.
A wish is completed if the savedAmount is greater or equal to the price of the wish.

==== Current Implementation


Given below is an example usage scenario and how the list overdue mechanism behaves at each step:

.  The user executes the command `list -c`.
.  `model.updateFilteredWishList()` will update the wish list with `WishCompletedPredicate` as the parameter (boolean). `wish.isFulfilled()` is called to check whether the wish is completed or not.
.  The updated wish list would be reflected on the UI to be displayed to the user.

The following sequence diagram shows how the Wish Detail Panel displays its updated content:

image::ListCompletedSequenceDiagram.png[width="800"]

=== Redesign of User Interface

The UI has been redesigned to implement the following UI components required for WishBook:

* Command Box
* Wish List Panel
* Wish Detail Panel


==== Wish List Panel

The Wish List Panel consists of a list of Wish Card which contains 4 UI elements:

* `WishCard#nameLabel` - A `Text` element that displays the wish’s name.
* `WishCard#progressLabel` - A `Text` element that displays the wish’s saving progress in percentage format.
* `WishCard#tags` - A `FlowPane` element that contains a `Text` element which displays the wish’s assigned tags.
* `WishCard#progressBar` - A `progressBar` element that visually presents the percentage of the wish’s current saving progress.

Whenever the user adds a new wish or edits an existing wish, a new WishCard containing the wish will be added to the Wish List Panel or the content in the existing WishCard will be updated respectively.

The user will be able to view the wish’s current saving progress both in terms of text on the progressLabel (e.g. ’80%’) and the progressBar. Also, the user will be able to see all the tags he/she assigned to categorize the wish.

===== Problem with the old design

The UI (MainWindow) constructs the `WishListPanel` by obtaining an `ObservableList` of wish cards from `Model`, this list is assigned when UI starts, and will never be re-assigned.
The UI "observes" the list and updates when it is modified.

This approach works well for the `WishListPanel` because WishBook contains only 1 list of wish cards.
However, the saving history list in the `WishDetailPanel` can not be updated in the same manner because Model component will change its card list’s reference when a user adds a new wish or updates the content of the wish.

In this case, the `WishDetailPanel` in UI will not be updated because the card list of which UI has reference to is actually not changed.

===== Design considerations

* **Alternative 1 (current choice)**: Have a wishList in `Model` and keep it updated with the current list of cards
** Explanation: The UI needs only 1 reference to this `wishList`, each time user executes any changes, `wishList` is cleared and the new list of cards is copy to the `wishList`.
** Pros: The structure of `Model` and UI component needs not be changed
** Cons: Need to keep a copy of the current card list, copying the whole list of cards for each command operation has bad effect on performance .

* **Alternative 2**: Model component raises an event when its current card list’s reference is changed
** Explanation: When user adds a new wish or executes save, `Model` will raise an event (`WishPanelUpdatedEvent`), which is subscribed by UI, then UI can re-assign its list of cards and update the cards panel accordingly.
** Pros: Better performance
** Cons: Need to re-design `Model` and UI components 

==== Wish Detail Panel

The Wish Detail Panel consists of 3 UI sub-components:

* `WishDetailSavingAmount` that contains `Text` elements to display price and the saving progress of the wish
* `WishDetailSavingHistory` that contains a `List` of history of saving inputs of the wish
* `WishBrowserPanel` that displays `WebView` of the URL of the wish.

Whenever the user adds a new wish or edits an existing wish, the content of the wish in Wish Detail Panel will be updated.
The user will be able to view the wish’s current saving progress and the history of his/her saving inputs of the wish in the list format.
Also, the user will be able to browse through the wish’s product page via its assigned URL.

===== Current Implementation

Every time a new Wish is added or an existing wish is updated by the commands such as save, it raises a `WishDataUpdatedEvent`.
The UI will then handle that event and update the `WishDetailPanel` with the new version of wish.

Given below is an example usage scenario and how the WishBook behaves and `WishDetailPanel` is updated at each step:

.  The user executes the command `save 1 1000`.

[NOTE]
If a command fails its execution, WishDataUpdatedEvent will not be posted.

.  The save command updates the model with the new wish and raises a new `WishDataUpdatedEvent`.
.  `WishDetailSavingAmount` and `WishDetailSavingHistory` responds to the `WishDataUpdatedEvent` with `WishListPanel#handleWishUpdatedEvent()`.
.  The `WishDetailSavingAmount` updates the wish’s current saving progress when `WishDetailSavingAmount#loadWishDetails` is called.
.  The progress is calculated from when `Wish#getProgress` is called. The value is saveAmount / price. Then the progress label for the wish is set to that fraction.
.  The `WishDetailSavingHistory` updates the wish’s saving history list when `WishDetailSavingHistory#loadWishDetails` is called.
.  The saving history list is cleared.
.  The new set of history entry is retrieved from `wishTransaction#getWishMap` and the saved amount is calculated from subtracting previous saving amount from the next one.
.  The saving history list is now filled with the new list of updated saving history.

The following sequence diagram shows how the Wish Detail Panel displays its updated content:

image::WishDetailPanelSequenceDiagram.png[width="800"]

===== Design Considerations

**Aspect: How to update the progress and saving history on UI**

* **Alternative 1 (current choice)**: Clear all the sub components and add new sub components accordingly
** Pros: No matter which progress or history is changed, or what type of change (ie. delete, add, or edit), this change can be handled by the same method each time.
** Cons: It is redundant to clear everything and replace them with new sub components.

* **Alternative 2**: Handle different kinds of changes to the progress or history lists.
** Pros: It is a lot faster to only change the sub component that is affected.
** Cons: There are too many cases for how the lists can be changed. (ie. a different change is needed for each of these cases: wish is deleted/edited/created/cleared, or a `wishTransaction` is deleted/added)
<file_sep>/src/test/java/seedu/address/model/ModelManagerTest.java
package seedu.address.model;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_FRIEND;
import static seedu.address.model.Model.PREDICATE_SHOW_ALL_WISHES;
import static seedu.address.testutil.TypicalWishes.ALICE;
import static seedu.address.testutil.TypicalWishes.BENSON;

import java.nio.file.Paths;
import java.util.Arrays;

import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;

import seedu.address.model.tag.Tag;
import seedu.address.model.wish.WishContainsKeywordsPredicate;
import seedu.address.testutil.WishBookBuilder;

public class ModelManagerTest {
    @Rule
    public ExpectedException thrown = ExpectedException.none();

    private ModelManager modelManager = new ModelManager();

    @Test
    public void hasWish_nullWish_throwsNullPointerException() {
        thrown.expect(NullPointerException.class);
        modelManager.hasWish(null);
    }

    @Test
    public void hasWish_wishNotInWishBook_returnsFalse() {
        assertFalse(modelManager.hasWish(ALICE));
    }

    @Test
    public void hasWish_wishInWishBook_returnsTrue() {
        modelManager.addWish(ALICE);
        assertTrue(modelManager.hasWish(ALICE));
    }

    @Test
    public void hasTagAfterRemoveTag() {
        Tag tag = new Tag(VALID_TAG_FRIEND);
        modelManager.addWish(BENSON);
        modelManager.deleteTag(tag);
        assertFalse(modelManager
                .getWishBook()
                .getWishList()
                .filtered(wish ->
                        wish.getTags().contains(tag)).size() > 0);
    }

    @Test
    public void getFilteredWishList_modifyList_throwsUnsupportedOperationException() {
        thrown.expect(UnsupportedOperationException.class);
        modelManager.getFilteredSortedWishList().remove(0);
    }

    @Test
    public void equals() {
        WishBook wishBook = new WishBookBuilder().withWish(ALICE).withWish(BENSON).build();
        WishTransaction wishTransaction = new WishTransaction(wishBook);
        WishBook differentWishBook = new WishBook();
        WishTransaction differentWishTransaction = new WishTransaction();
        UserPrefs userPrefs = new UserPrefs();

        // same values -> returns true
        modelManager = new ModelManager(wishBook, wishTransaction, userPrefs);
        ModelManager modelManagerCopy = new ModelManager(wishBook, wishTransaction, userPrefs);
        assertTrue(modelManager.equals(modelManagerCopy));

        // same object -> returns true
        assertTrue(modelManager.equals(modelManager));

        // null -> returns false
        assertFalse(modelManager.equals(null));

        // different types -> returns false
        assertFalse(modelManager.equals(5));

        // different wishBook -> returns false
        assertFalse(modelManager.equals(new ModelManager(differentWishBook, differentWishTransaction, userPrefs)));

        // different filteredList -> returns false
        String[] nameKeywords = ALICE.getName().fullName.split("\\s+");
        modelManager.updateFilteredWishList(new WishContainsKeywordsPredicate(Arrays.asList(nameKeywords),
                Arrays.asList(), Arrays.asList(), true));
        assertFalse(modelManager.equals(new ModelManager(wishBook, wishTransaction, userPrefs)));

        // resets modelManager to initial state for upcoming tests
        modelManager.updateFilteredWishList(PREDICATE_SHOW_ALL_WISHES);

        // different userPrefs -> returns true
        UserPrefs differentUserPrefs = new UserPrefs();
        differentUserPrefs.setWishBookFilePath(Paths.get("differentFilePath"));
        assertTrue(modelManager.equals(new ModelManager(wishBook, wishTransaction, differentUserPrefs)));
    }
}
<file_sep>/src/test/java/seedu/address/logic/parser/RemarkCommandParserTest.java
package seedu.address.logic.parser;

import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
import static seedu.address.logic.commands.CommandTestUtil.REMARK_DESC_EMPTY;
import static seedu.address.logic.commands.CommandTestUtil.REMARK_DESC_SAMPLE_1;
import static seedu.address.logic.commands.CommandTestUtil.REMARK_DESC_SAMPLE_2;
import static seedu.address.logic.commands.CommandTestUtil.SAMPLE_REMARK_1;
import static seedu.address.logic.commands.CommandTestUtil.SAMPLE_REMARK_EMPTY;
import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure;
import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess;
import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_WISH;

import org.junit.Test;

import seedu.address.logic.commands.RemarkCommand;

public class RemarkCommandParserTest {
    private static final String MESSAGE_INVALID_FORMAT = String.format(
            MESSAGE_INVALID_COMMAND_FORMAT, RemarkCommand.MESSAGE_USAGE);

    private RemarkCommand expectedCommand = new RemarkCommand(INDEX_FIRST_WISH, SAMPLE_REMARK_1);
    private RemarkCommandParser parser = new RemarkCommandParser();

    @Test
    public void parse_allFieldsPresent_success() {
        // empty remark
        assertParseSuccess(parser, INDEX_FIRST_WISH.getOneBased() + REMARK_DESC_EMPTY,
                new RemarkCommand(INDEX_FIRST_WISH, SAMPLE_REMARK_EMPTY));

        // no empty fields
        assertParseSuccess(parser, INDEX_FIRST_WISH.getOneBased() + REMARK_DESC_SAMPLE_1,
                expectedCommand);
    }

    @Test
    public void parse_invalidPreamble_failure() {
        // empty index
        assertParseFailure(parser, "", MESSAGE_INVALID_FORMAT);

        // negative index
        assertParseFailure(parser, "-5" + REMARK_DESC_SAMPLE_1, MESSAGE_INVALID_FORMAT);

        //zero index
        assertParseFailure(parser, "0" + REMARK_DESC_SAMPLE_1, MESSAGE_INVALID_FORMAT);

        // invalid arguments being parsed as preamble
        assertParseFailure(parser, "5 random string" + REMARK_DESC_SAMPLE_1, MESSAGE_INVALID_FORMAT);
    }

    @Test
    public void parse_multipleRepeatedFields_acceptsLast() {
        // remark is repeated twice -> second remark is taken
        assertParseSuccess(parser, INDEX_FIRST_WISH.getOneBased()
                + REMARK_DESC_SAMPLE_2 + REMARK_DESC_SAMPLE_1, expectedCommand);
    }

    @Test
    public void parse_missingParts_failure() {
        // missing index
        assertParseFailure(parser, REMARK_DESC_SAMPLE_1, MESSAGE_INVALID_FORMAT);

        // missing remark
        assertParseFailure(parser, "" + INDEX_FIRST_WISH.getOneBased(),
                MESSAGE_INVALID_FORMAT);
    }
}
<file_sep>/src/main/java/seedu/address/model/Model.java
package seedu.address.model;

import java.util.function.Predicate;

import javafx.collections.ObservableList;
import seedu.address.commons.core.amount.Amount;
import seedu.address.model.wish.Wish;

/**
 * The API of the Model component.
 */
public interface Model {
    /** {@code Predicate} that always evaluate to true */
    Predicate<Wish> PREDICATE_SHOW_ALL_WISHES = unused -> true;

    /** Returns the WishTransaction */
    WishTransaction getWishTransaction();

    /** Clears existing backing model and replaces with the provided new data. */
    void resetData(ReadOnlyWishBook newData);

    /** Returns the unusedFunds of the WishBook. */
    String getUnusedFunds();

    /** Returns the WishBook. */
    ReadOnlyWishBook getWishBook();

    /**
     * Returns true if a wish with the same identity as {@code wish} exists in the wish book.
     */
    boolean hasWish(Wish wish);

    /**
     * Deletes the given wish.
     * The wish must exist in the wish book.
     */
    void deleteWish(Wish target);

    /**
     * Adds the given wish.
     * {@code wish} must not already exist in the wish book.
     */
    void addWish(Wish wish);

    /**
     * Replaces the given wish {@code target} with {@code editedWish}.
     * {@code target} must exist in the wish book.
     * The wish identity of {@code editedWish} must not be the same as another existing wish in the wish book.
     */
    void updateWish(Wish target, Wish editedWish);

    /**
     * Updates the unusedFunds amount by the given {@code change}
     * @code change The amount to update unusedFunds by.
     */
    void updateUnusedFunds(Amount change);

    /** Returns an unmodifiable view of the filtered wish list */
    ObservableList<Wish> getFilteredSortedWishList();

    /**
     * Updates the filter of the filtered wish list to filter by the given {@code predicate}.
     * @throws NullPointerException if {@code predicate} is null.
     */
    void updateFilteredWishList(Predicate<Wish> predicate);

    /**
     * Returns true if the model has previous wish book states to restore.
     */
    boolean canUndoWishBook();

    /**
     * Returns true if the model has undone wish book states to restore.
     */
    boolean canRedoWishBook();

    /**
     * Restores the model's wish book to its previous state.
     */
    void undoWishBook();

    /**
     * Restores the model's wish book to its previously undone state.
     */
    void redoWishBook();

    /**
     * Saves the current wish book state for undo/redo.
     */
    void commitWishBook();


}
<file_sep>/docs/DeveloperGuide.adoc
= WishBook - Developer Guide
:site-section: DeveloperGuide
:toc:
:toc-title:
:toc-placement: preamble
:sectnums:
:imagesDir: images
:stylesDir: stylesheets
:xrefstyle: full
ifdef::env-github[]
:tip-caption: :bulb:
:note-caption: :information_source:
:warning-caption: :warning:
:experimental:
endif::[]
:repoURL: https://github.com/CS2103-AY1819S1-T16-1/main

By: `Team SE-EDU`      Since: `Jun 2016`      Licence: `MIT`

== Setting up

=== Prerequisites

. *JDK `9`* or later
+
[WARNING]
JDK `10` on Windows will fail to run tests in <<UsingGradle#Running-Tests, headless mode>> due to a
https://github.com/javafxports/openjdk-jfx/issues/66[JavaFX bug].
Windows developers are highly recommended to use JDK `9`.

. *IntelliJ* IDE
+
[NOTE]
IntelliJ by default has Gradle and JavaFx plugins installed. +
Do not disable them. If you have disabled them, go to `File` > `Settings` > `Plugins` to re-enable them.


=== Setting up the project in your computer

. Fork this repo, and clone the fork to your computer
. Open IntelliJ (if you are not in the welcome screen, click `File` > `Close Project` to close the existing project
dialog first)
. Set up the correct JDK version for Gradle
.. Click `Configure` > `Project Defaults` > `Project Structure`
.. Click `New...` and find the directory of the JDK
. Click `Import Project`
. Locate the `build.gradle` file and select it. Click `OK`
. Click `Open as Project`
. 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/url/storage/XmlAdaptedWish.java[`XmlAdaptedWish.java`] and
link:{repoURL}/src/main/java/seedu/url/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/url/commons/util/XmlUtilTest.java[`XmlUtilTest.java`]
and link:{repoURL}/src/test/java/seedu/url/ui/HelpWindowTest.java[`HelpWindowTest.java`] for code errors, and if so,
resolve it the same way)

=== Verifying the setup

. Run the `seedu.url.MainApp` and try a few commands
. <<Testing,Run the tests>> to ensure they all pass.

=== Configurations to do before writing code

==== Configuring the coding style

This project follows https://github.com/oss-generic/process/blob/master/docs/CodingStandards.adoc[oss-generic coding standards]. IntelliJ's default style is mostly compliant with ours but it uses a different import order from ours. To rectify,

. Go to `File` > `Settings...` (Windows/Linux), or `IntelliJ IDEA` > `Preferences...` (macOS)
. Select `Editor` > `Code Style` > `Java`
. Click on the `Imports` tab to set the order

* For `Class count to use import with '\*'` and `Names count to use static import with '*'`: Set to `999` to prevent IntelliJ from contracting the import statements
* For `Import Layout`: The order is `import static all other imports`, `import java.\*`, `import javax.*`, `import org.\*`, `import com.*`, `import all other imports`. Add a `<blank line>` between each `import`

Optionally, you can follow the <<UsingCheckstyle#, UsingCheckstyle.adoc>> document to configure Intellij to check style-compliance as you write code.

==== Updating documentation to match your fork

After forking the repo, the documentation will still have the SE-EDU branding and refer to the `CS2103-AY1819S1-T16-1/main` repo.

If you plan to develop this fork as a separate product (i.e. instead of contributing to `CS2103-AY1819S1-T16-1/main`), you should do the following:

. Configure the <<Docs-SiteWideDocSettings, site-wide documentation settings>> in link:{repoURL}/build.gradle[`build.gradle`], such as the `site-name`, to suit your own project.

. Replace the URL in the attribute `repoURL` in link:{repoURL}/docs/DeveloperGuide.adoc[`DeveloperGuide.adoc`] and link:{repoURL}/docs/UserGuide.adoc[`UserGuide.adoc`] with the URL of your fork.

==== Setting up CI

Set up Travis to perform Continuous Integration (CI) for your fork. See <<UsingTravis#, UsingTravis.adoc>> to learn how to set it up.

After setting up Travis, you can optionally set up coverage reporting for your team fork (see <<UsingCoveralls#, UsingCoveralls.adoc>>).

[NOTE]
Coverage reporting could be useful for a team repository that hosts the final version but it is not that useful for your personal fork.

Optionally, you can set up AppVeyor as a second CI (see <<UsingAppVeyor#, UsingAppVeyor.adoc>>).

[NOTE]
Having both Travis and AppVeyor ensures your App works on both Unix-based platforms and Windows-based platforms (Travis is Unix-based and AppVeyor is Windows-based)

==== Getting started with coding

When you are ready to start coding,

1. Get some sense of the overall design by reading <<Design-Architecture>>.
2. Take a look at <<GetStartedProgramming>>.

== Design

[[Design-Architecture]]
=== Architecture

.Architecture Diagram
image::Architecture.png[width="600"]

The *_Architecture Diagram_* given above explains the high-level design of the App. Given below is a quick overview of each component.

[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/url/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.

<<Design-Commons,*`Commons`*>> represents a collection of classes used by multiple other components. Two of those classes play important roles at the architecture level.

* `EventsCenter` : This class (written using https://github.com/google/guava/wiki/EventBusExplained[Google's Event Bus library]) is used by components to communicate with other components using events (i.e. a form of _Event Driven_ design)
* `LogsCenter` : Used by many classes to write log messages to the App's log file.

The rest of the App consists of four components.

* <<Design-Ui,*`UI`*>>: The UI of the App.
* <<Design-Logic,*`Logic`*>>: The command executor.
* <<Design-Model,*`Model`*>>: Holds the data of the App in-memory.
* <<Design-Storage,*`Storage`*>>: Reads data from, and writes data to, the hard disk.

Each of the four components

* Defines its _API_ in an `interface` with the same name as the Component.
* Exposes its functionality using a `{Component Name}Manager` class.

For example, the `Logic` component (see the class diagram given below) defines it's API in the `Logic.java` interface and exposes its functionality using the `LogicManager.java` class.

.Class Diagram of the Logic Component
image::LogicClassDiagram.png[width="800"]

[discrete]
==== Events-Driven nature of the design

The _Sequence Diagram_ below shows how the components interact for the scenario where the user issues the command `delete 1`.

.Component interactions for `delete 1` command (part 1)
image::SDforDeleteWish.png[width="800"]

[NOTE]
Note how the `Model` simply raises a `WishBookChangedEvent` when the Wish Book data is changed, instead of asking the `Storage` to save the updates to the hard disk. This event also triggers the save of wish histories to disk.

The diagram below shows how the `EventsCenter` reacts to that event, which eventually results in the updates being saved to the hard disk and the status bar of the UI being updated to reflect the 'Last Updated' time.

.Component interactions for `delete 1` command (part 2)
image::SDforDeleteWishEventHandling.png[width="800"]

[NOTE]
Note how the event is propagated through the `EventsCenter` to the `Storage` and `UI` without `Model` having to be coupled to either of them. This is an example of how this Event Driven approach helps us reduce direct coupling between components.

The sections below give more details of each component.

[[Design-Ui]]
=== UI component

.Structure of the UI Component
image::UiClassDiagram.png[width="800"]

*API* : link:{repoURL}/src/main/java/seedu/url/ui/Ui.java[`Ui.java`]

The UI consists of a `MainWindow` that is made up of parts e.g.`CommandBox`, `ResultDisplay`, `WishListPanel`, `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/url/ui/MainWindow.java[`MainWindow`] is specified in link:{repoURL}/src/main/resources/view/MainWindow.fxml[`MainWindow.fxml`]

The `UI` component,

* Executes user commands using the `Logic` component.
* Binds itself to some data in the `Model` so that the UI can auto-update when data in the `Model` change.
* Responds to events raised from various parts of the App and updates the UI accordingly.

[[Design-Logic]]
=== Logic component

[[fig-LogicClassDiagram]]
.Structure of the Logic Component
image::LogicClassDiagram.png[width="800"]

*API* :
link:{repoURL}/src/main/java/seedu/url/logic/Logic.java[`Logic.java`]

.  `Logic` uses the `WishBookParser` 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 wish) and/or raise events.
.  The result of the command execution is encapsulated as a `CommandResult` object which is passed back to the `Ui`.

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"]

[[Design-Model]]
=== Model component

.Structure of the Model Component
image::ModelClassDiagram.png[width="800"]

*API* : link:{repoURL}/src/main/java/seedu/url/model/Model.java[`Model.java`]

The `Model`,

* stores a `UserPref` object that represents the user's preferences.
* stores the Wish Book data.
* stores the data of wish histories.
* exposes an unmodifiable `ObservableList<Wish>` 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. The elements of the `ObservableList<Wish>` can also
be filtered and sorted to suit the needs of specific commands.
* does not depend on any of the other three components.

[NOTE]
As a more OOP model, we can store a `Tag` list in `Wish Book`, which `Wish` can reference. This would allow `Wish Book` to only require one `Tag` object per unique `Tag`, instead of each `Wish` needing their own `Tag` object. An example of how such a model may look like is given below. +
 +
image:ModelClassBetterOopDiagram.png[width="800"]

[[Design-Storage]]
=== Storage component

.Structure of the Storage Component
image::StorageClassDiagram.png[width="800"]

*API* : link:{repoURL}/src/main/java/seedu/url/storage/Storage.java[`Storage.java`]

The `Storage` component,

* can save `UserPref` objects in json format and read it back.
* can save the Wish Book data in xml format and read it back.

[[Design-Commons]]
=== Common classes

Classes used by multiple components are in the `seedu.WishBook.commons` package.

== Implementation

This section describes some noteworthy details on how certain features are implemented.

// tag::implementationmodel[]

=== Model
==== Wish Model
A wish is uniquely identified by its Universal Unique Identifier (UUID) which is generated randomly only once for a
particular wish, upon its creation through the `AddCommand`. A wish stores the following primary attributes:

* Name
* Price
* Date
* Saved Amount
* Url
* Remark
* Tags
* UUID

[NOTE]
It is impossible for the user to create a duplicate wish as it is impossible to modify a wish's UUID.

==== Wish Priority
A wish needs to be prioritised in a specific order such that the wishes with the highest priority will be visible
on the top of the list. In WishBook, the priority is determined primarily by the due date of the wish which is
stored in every wish's `Date` attribute. Ties are broken by `Name`. Further ties are broken by `UUID` as it is possible
for the `Date` and `Name` of two wishes to be identical.

The sorting of the displayed results is done by the `filteredSortedWishes` list. The sorting order is specified by
`WishComparator`.

==== Design Considerations
===== Aspect: Uniqueness of a Wish
* **Alternative 1(current choice):** Identify a `Wish` by a randomly generated UUID.
** Pros: Extremely low probability of collision.
** Pros: No extra maintenance required upon generation as every `Wish` is unique.
** Cons: UUID does not map to any real world entity and it is used strictly for identification.
** Cons: It is more difficult to system test the `AddCommand` with the current group of methods for system tests as
UUID is randomly generated each time.

* **Alternative 2:** Identify a wish by `Name`, `Price`, `Date`, `Url`, `Tags`. Wishes with identical values for
these attributes will be represented by a single `WishCard`. The `WishCard` will be augmented with a `Multiplicity` to
indicate the number of identical wishes.
** Pros: WishBook will be more compact and every attribute stored in a `Wish` maps to a real entity.
** Cons: Additional attribute `Multiplicity` may have to be frequently edited as it is another
attribute that is affected by multiple commands.

* **Alternative 3:** Identify a wish by a new attribute `CreatedTime`, which is derived from the system time
when the wish is created.
** Pros: The attribute maps to a real entity. It can be an additional information presented to the user about a wish.
** Cons: There might be collisions in `CreatedTime` if the the system time is incorrect.
// end::implementationmodel[]

// tag::undoredo[]
=== Undo/Redo feature
==== Current Implementation

The undo/redo mechanism is facilitated by `VersionedWishBook`.
It extends `WishBook` with an undo/redo history, stored internally as an `WishBookStateList` and `currentStatePointer`.
Additionally, it implements the following operations:

* `VersionedWishBook#commit()` -- Saves the current wish book state in its history.
* `VersionedWishBook#undo()` -- Restores the previous wish book state from its history.
* `VersionedWishBook#redo()` -- Restores a previously undone wish book state from its history.

These operations are exposed in the `Model` interface as `Model#commitWishBook()`, `Model#undoWishBook()` and `Model#redoWishBook()` 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 `VersionedWishBook` will be initialized with the initial wish book state, and the `currentStatePointer` pointing to that single url book state.

image::UndoRedoStartingStateListDiagram.png[width="800"]

Step 2. The user executes `delete 5` command to delete the 5th wish in the wish book. The `delete` command calls `Model#commitWishBook()`, causing the modified state of the url book after the `delete 5` command executes to be saved in the `WishBookStateList`, and the `currentStatePointer` is shifted to the newly inserted url book state.

image::UndoRedoNewCommand1StateListDiagram.png[width="800"]

Step 3. The user executes `add n/David ...` to add a new wish. The `add` command also calls `Model#commitWishBook()`, causing another modified wish book state to be saved into the `WishBookStateList`.

image::UndoRedoNewCommand2StateListDiagram.png[width="800"]

[NOTE]
If a command fails its execution, it will not call `Model#commitWishBook()`, so the wish book state will not be saved into the `WishBookStateList`.

Step 4. The user now decides that adding the wish was a mistake, and decides to undo that action by executing the `undo` command. The `undo` command will call `Model#undoWishBook()`, which will shift the `currentStatePointer` once to the left, pointing it to the previous wish book state, and restores the url book to that state.

image::UndoRedoExecuteUndoStateListDiagram.png[width="800"]

[NOTE]
If the `currentStatePointer` is at index 0, pointing to the initial wish book state, then there are no previous url book states to restore. The `undo` command uses `Model#canUndoWishBook()` 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#redoWishBook()`, which shifts the `currentStatePointer` once to the right, pointing to the previously undone state, and restores the wish book to that state.

[NOTE]
If the `currentStatePointer` is at index `WishBookStateList.size() - 1`, pointing to the latest wish book state, then there are no undone url book states to restore. The `redo` command uses `Model#canRedoWishBook()` 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 wish book, such as `list`, will usually not call `Model#commitWishBook()`, `Model#undoWishBook()` or `Model#redoWishBook()`. Thus, the `WishBookStateList` remains unchanged.

image::UndoRedoNewCommand3StateListDiagram.png[width="800"]

Step 6. The user executes `clear`, which calls `Model#commitWishBook()`. Since the `currentStatePointer` is not pointing at the end of the `WishBookStateList`, all wish 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"]

The following activity diagram summarizes what happens when a user executes a new command:

image::UndoRedoActivityDiagram.png[width="650"]

==== Design Considerations

===== Aspect: How undo & redo executes

* **Alternative 1 (current choice):** Saves the entire wish 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.
** Pros: Will use less memory (e.g. for `delete`, just save the wish being deleted).
** Cons: We must ensure that the implementation of each individual command are correct.

===== Aspect: Data structure to support the undo/redo commands

* **Alternative 1 (current choice):** Use a list to store the history of wish 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 `VersionedWishBook`.
* **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::savingsHistory[]
=== Savings History feature
==== Capturing the state of `WishTransaction`

The current state of the savings history of the `WishBook` is captured by `VersionedWishTransaction`.
`VersionedWishTransaction` extends `WishTransaction` and has an undo/redo history, similar to the implementation of the Undo/Redo feature, and is stored internally as a `wishStateList` and `currentStatePointer`. Additionally, it implements `VersionedModel` and so contains the implementation of the following operations:

* `VersionedWishTransaction#commit()` -- Saves the current wish transaction state in its history.
* `VersionedWishTransaction#undo()` -- Restores the previous wish transaction state from its history.
* `VersionedWishTransaction#redo()` -- Restores a previously undone wish transaction state from its history.

These operations are exposed in the `Model` interface as `Model#commitWishBook()`, `Model#undoWishBook()` and `Model#redoWishBook()` respectively.

==== Capturing the state of each `Wish`

`WishTransaction` keeps track of the state of all wishes in `WishBook` via a `wishMap` which maps the unique ID of a `Wish` to a list of `Wish` states. `WishTransaction` implements `ActionCommandListener` such that any state changing command performed to a `Wish` or the `WishBook` such as `AddCommand()`, `EditCommand()`, `SaveCommand()`, etc will result in the `WishMap` being updated accordingly in `WishTransaction`.

==== Persistent storage

`VersionedWishTransaction`, `WishTransaction` can be easily converted to and from xml using  `XmlWishTransactions`. `XmlWishTransactions` is saved as an xml file when the user explicitly closes the window, thereby invoking `MainApp#stop()` which saves the current state of `VersionedWishTransaction` in the `wishStateList` to hard disk.

If the user's command triggers a change in the state of the `WishBook`, a `WishBookChangedEvent` will be raised, causing the subscribed `StorageManager` to respond by saving both the current state of the `WishBook` and `WishTransaction` to disk.

Given below is an example usage scenario and how the savings history mechanism behaves at each step.

Step 1. The user launches the application. The default file path storing the previous state of the `WishTransaction` will be retrieved, unless otherwise specified by the user, and the contents from the xml file will be parsed and converted into a `WishTransaction` object via the `XmlWishTransactions` object. If the file at the specified location is behind the current state of the `WishBook`, content of the `WishTransaction` will be overwritten by the `WishBook`.

[NOTE]
The `wishStateList` starts off with the initial state of the `WishTransaction` as the first item in the list.

Step 2. The user executes `add n/iPhone ...` to add a new wish. The `add` command calls `Model#commitWishBook()`, causing the current state of the modified wish transaction state to be saved into `wishStateList`. As this is a command that changes the state of the `WishBook`, `Model#addWish()` will call `VersionedWishTransaction#addWish()` to add a new wish to the `WishMap`.

[NOTE]
* If a command fails its execution, it will not call `Model#commitWishBook()`, so the wish transaction state will not be saved into the `wishStateList`.
* If the `WishMap` contains an identical wish (such is identified by `Wish#isSameWish()`), then the call to add this wish will fail. As such, the wish will not be added to the `WishMap` or the `WishBook`.

Step 3. The user now decides that adding the wish was a mistake, and decides to undo that action by executing the `undo` command. The `undo` command will call `Model#undoWishBook()`, which will shift the `currentStatePointer` once to the left, pointing it to the previous wish transaction state, and restores the wish transaction to that state.

[NOTE]
If the `currentStatePointer` is at index 0, pointing to the initial wish transaction state, then there are no previous wish transaction states to restore. The `undo` command uses `Model#canUndoWishBook()` 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 `redo` command does the opposite -- it calls `Model#redoWishBook()`, which shifts the `currentStatePointer` once to the right, pointing to the previously undone state, and restores the wish transaction to that state.

[NOTE]
If the `currentStatePointer` is at index `wishStateList.size() - 1`, pointing to the latest wish transaction state, then there are no undone wish transaction states to restore. The `redo` command uses `Model#canRedoWishBook()` 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 4. The user then decides to execute the command `list`. Commands that do not modify the state of the `WishBook`, such as `list`, will usually not call `Model#commitWishBook()`, `Model#undoWishBook()` or `Model#redoWishBook()`. Thus, the `WishBookStateList` remains unchanged.

Step 5. The user finally exits the app by clicking on the close button. The most recent state of the `WishTransaction` will be converted into xml format via the the `XmlWishTransactions` object and be saved into the same file path it was first retrieved from.

[NOTE]
If there was some error saving the current state of the `WishTransaction` to the specified file path in hard disk, an exception will be thrown and a warning will be shown to the user. The current state of the `WishTransaction` object will not be saved to hard disk.
// end::savingsHistory[]

// tag::wish[]
=== Add Wish feature

==== Current Implementation

The Add Wish feature is executed through an `AddCommand` by the user, which after parsing,
is facilitated mainly by the `ModelManager` which implements `Model`.
It also affects `versionedWishBook` and `versionedWishTransaction` by adding the
resultant wish to both of their respective data structures.
After adding a `Wish`, the `filteredSortedWishes` is also updated to reflect the
latest version of WishBook.
The UI is also prompted to refresh through a `WishBookChangedEvent`.

`AddCommandParser` parses the user's input for parameters using prefixes,
and checks them against their respective regular expressions (regex), specified
in their respective classes.

====

The following prefix/parameter pairs are **compulsory**, where a user's input will be
rejected if they are not provided:

* `n/`: Name
* `p/`: Price
* One of the following Date parameters:
** `d/`: Exact expiry date
** `a/`: duration (or lifetime) from time when command is entered

The following prefix/parameter pairs are **optional**, where a user's input will
be successful even if they are not provided:

* `t/`: tags (more than one allowed)
* `u/`: product's URL (product page)

====

[NOTE]
====
**Regarding Duration (`a/`) vs Date (`d/`)**

* If `d/` is used, a valid Date should be used.
** Date comes in the format of `dd/mm/yyyy`,
`dd` being days, `mm` being months, `yyyy` being the year, and the
** Specified date should also be a valid date in the future.
* If `a/` is used, a valid Duration should be used.
** length instead of `dd/mm/yyyy` format, the format should be `<years>y<months>m<days>d`.
* In any command, only `Duration` or `Date` can be used. Never both.
* If an invalid string for date or duration is provided, a warning will be
displayed to prompt the user to enter a valid date or duration.

====

Given below is an example usage scenario and how an AddCommand is carried out.

Step 1. The user types in a valid `AddCommand`, `add n/1 TB Toshiba SSD p/158 a/200d`, and the current date is 2nd October 2017 (2/10/2017).

The `AddCommandParser` will employ `ParserUtil` to parse the attributes specified after each prefix. The parsing of the
`Duration` attribute which follows `a/` in the command will be discussed below.

Since `Duration` prefix is used, the computation of a wish's expiry date is handled
internally in the `ParserUtil` class, which `ParserUtil#parseDate()` parses and converts
the input string into a `Period` object (if input is valid),
and adds the resultant `Period` to the current `Date` to get the desired
`Date` of the `Wish`.

The resultant `Wish` will have the following properties:

* id: `a randomly-generated UUID`
* Name: _1TB Toshiba SSD_
* SavedAmount: 0.00
* Price: 158.00
* Date: 20/4/2018 (20th April 2018)
* URL: `empty string`
* Remark: `empty string`
* Tags: `none`
* Fulfilled: `false`
* Expired: `false`

The resultant wish is pass into `VersionedWishBook#addWish` and `VersionedWishTransaction#addWish`,
which tracks the history of the `WishBook` and `Wish` respectively. The list of wishes shown on the UI is also updated to show all wishes again,
as `filteredSortedWishes` is updated to have all wishes in `WishBook` and a `WishBookChangedEvent` is fired.

The following sequence diagram shows how an `AddCommand` is processed in WB:

image::AddWishSequenceDiagram.png[width="800"]

Step 2. Some time later, the user decides that she wants the exact same wish,
but duplicated, and enters the exact same command, but with an exact `Date`
instead of `Duration`, so the command entered is
`add n/1 TB Toshiba SSD p/158 d/20/4/2018`.

Since `Date` prefix is used, the `ParserUtil` parses the string into a `Date` object,
and the resultant object is used directly for the resultant `Wish`.

Similar to in Step 1, the command will be parsed successfully and a second `Wish`
will be added, albeit with a different (hidden) id generated.

The resultant `Wish` will have the following properties:

* id: `another randomly-generated UUID`
* Name: _1TB Toshiba SSD_
* SavedAmount: 0.00
* Price: 158.00
* Date: 20/4/2018 (20th April 2018)
* URL: `empty string`
* Remark: `empty string`
* Tags: `none`
* Fulfilled: `false`
* Expired: `false`

==== Design Considerations
* **Alternative 1 (current choice)**: Different prefixes for `Duration` and `Date`.
** Pros: More focused user experience.
User get more specific feedback depending on their preferred way of inputting date if a wrong input was made.
If user uses `a/` and enters an incorrect `Duration`, the user will not receive an error message about the correct format
for an exact `Date`, and will only be notified of the correct format of a `Duration`.
** Pros: Easier to implement and handle isolate errors related to respective input parameters.
** Cons: More prefixes for user to remember.

* **Alternative 2**: Have `Duration` and `Date` use the same prefix.
** Pros: More natural usage of one prefix to determine `Wish` 's desired expiry date.
** Cons: Conflating implementation of `Duration` and `Date`, hence harder to debug.
** Cons: Tricky to implement, as we are parsing one input for two different desired formats.

// end::wish[]

// tag::save[]
=== Save Amount feature

==== Current Implementation

The Save Amount feature is executed through a `SaveCommand` by the user, which after parsing,
is facilitated mainly by the `ModelManager` which implements `Model`.
Wish stores the `price` and `savedAmount` of `Wish`, helping to track the progress of the savings towards the `price`.
Meanwhile, WishBook stores an `unusedFunds`, which is an unallocated pool of funds that can be used in the future.
After adding a saving, the `filteredSortedWishes` in `ModelManager` is updated to reflect the latest observable WishBook.

Given below is an example usage scenario and how the SaveCommand behaves at each step:

Step 1. The user executes `save 1 10`, to save $10 into an existing wish with `Index` 1 and `Price` $15. The $10 is
wrapped in an `Amount` and a `SaveCommand` instance is created with the `Amount`. `Amount` is then used to make an updated
instance of the `Wish` at index 1 whose `SavedAmount` will be updated. `Model#updateWish` is then called to update this
wish with the old one in `WishBook`.

[NOTE]
The `Index` of each `Wish` is labelled at the side of the app.

The resultant wish will have the following properties:

* Name: _1TB Toshiba SSD_
* SavedAmount: 10.00
* Price: 15.00
* Date: 20/4/2018 (20th April 2018)
* URL: `empty string`
* Remark: `empty string`
* Tags: `none`
* Fulfilled: `false`
* Expired: `false`

[NOTE]
`Amount` to be saved can be a negative value where it would mean a withdrawal of money from a particular wish.

[NOTE]
`SavedAmount` of a wish cannot be negative. This means that an `Amount` cannot be negative enough to cause `SavedAmount`
to be negative.

Step 2. The user decides to execute `save 1 10` again. However, SaveCommand checks that `savedAmount` > `price`.
SaveCommand#execute creates a new updated `Wish` with `savedAmount = wish.getPrice()`.

The resultant wish will have the following properties:

* Name: _1TB Toshiba SSD_
* SavedAmount: 15.00
* Price: 15.00
* Date: 20/4/2018 (20th April 2018)
* URL: `empty string`
* Remark: `empty string`
* Tags: `none`
* Fulfilled: `true`
* Expired: `false`

Step 3. The excess amount of $5 is stored in a new `Amount` variable `excess`.
SaveCommand#execute then calls Model#updateUnusedFunds(excess) to update the `unusedFunds` in WishBook.

In WishBook, the result would be:

* unusedFunds: 5.00

Step 4. The user tries to execute `save 1 10` again. However, since the value for Wish#isFulfilled is true, the amount
will not be saved. SaveCommand#execute will throw a CommandException, with the message "Wish has already been fulfilled!".

The following sequence diagram shows how the save operation works:

image::SaveCommandSequenceDiagram.png[width="800"]

==== Design Considerations
===== Aspect: Data structure to support the unusedFunds feature

* **Alternative 1 (current choice):** Store it in a `SavedAmount` variable in `WishBook`.
** Pros: Easy to implement.
** Cons: More methods needed when needing to move funds from `unusedFunds` to other wishes.
* **Alternative 2:** Store it as a pseudo wish with index 0.
** Pros: It can be treated as another `wish`, hence existing methods can be used without needing to create much
more new ones.
** Cons: Requires dealing with an extra wish that has to be hidden on the `WishListPanel` and displayed separately
on the UI.
We must remember to skip this wish in methods that involve displaying the WishList.

// end::save[]

// tag::find[]
=== Find Wish Feature

==== Current Implementation

The find mechanism is supported by `FindCommandParser`. It implements `Parser` that implements the following operation:

* `FindCommandParser#parse()` -- Checks the arguments for empty strings and throws a `ParseException` if empty string is
found. It then splits the arguments using `ArgumentTokenizer#tokenize()` and returns an `ArgumentMultimap`. Keywords of
the same prefix are then grouped using `ArgumentMultimap#getAllValues()`.

The find mechanism is also facilitated by `FindCommand`. It extends `Command` and implements the
following operation:

* `FindCommand#execute()` -- Executes the command by updating the current `FilteredSortedWishList` with the
`WishContainsKeywordPredicate`.

The predicate `WishContainsKeywordsPredicate`, takes in three lists of the keywords for the following attributes:

* Name
* Tags
* Remark

and also the `isExactMatch` argument. The result of the predicate is determined by checking
whether a `Wish` contains the given keywords at their corresponding attributes. The match threshold
is dictated by the value of `isExactMatch`.

==== Example

Given below is an example usage scenario and how the Find mechanism behaves at each step.

Step 1. The user launches the application for the first time.

Step 2. The user executes `find n/wat n/apple t/impor` command to get all wishes whose name contains the
keywords 'iphone' or 'tablet'.

Step 3. The `FindCommandParser#parse()` is called and the `WishContainsKeywordPredicate` is constructed with
the arguments of the find command.

Step 4. `FindCommand#execute()` is then called.

Step 5. The entire list of wishes is filtered by the predicate `WishContainsKeywordsPredicate`.

Step 6. The filtered list of wishes is returned to the GUI.

image::FindCommandSequenceDiagram.png[width="800"]

==== Design Considerations

===== Aspect: Argument format

* **Alternative 1 (Current choice):** Require the user to prepend every keyword argument with the appropriate
Wish attribute prefix.
** Pros: Easier to implement as it easier to match keyword against a Wish if the attribute to match against is known.
** Pros: User has more control over the results returned.
** Cons: User is required to type slightly more.

* **Alternative 2:** No prefixes are required in the arguments. Keywords can match with any one of the
following chosen wish attributes: `Name`, `Tags` or `Remark`.
** Pros: Less typing required from user.
** Cons: Command might be slightly slower as every keyword has to be checked against all chosen attributes of the
wish.
** Cons: User has less control over the results returned.

==== Aspect: Default threshold for match without the exact match flag
* **Alternative 1 (Current choice):** Keywords appended to different prefixes are grouped with a logical AND and
keywords appended to the same prefixes are grouped with a logical OR when being matched against a `Wish`.
** Pros: A more intuitive way to find wishes.
** Cons: Can be restrictive in some situations.

* **Alternative 2:** Keywords appended to different prefixes are grouped with a logical OR and
keywords appended to the same prefixes are grouped with a logical OR when being matched against a `Wish`.
** Pros: Search results will be more inclusive.
** Cons: Very slim chance for such a use case.
// end::find[]

// tag::list[]
=== List feature

The `list -c and list -u` command allows the user to view the list of all wishes, completed and ongoing, respectively.
A wish is completed if the savedAmount is greater or equal to the price of the wish.

==== Current Implementation


Given below is an example usage scenario and how the list overdue mechanism behaves at each step:

.  The user executes the command `list -c`.
.  `model.updateFilteredWishList()` will update the wish list with `WishCompletedPredicate` as the parameter (boolean). `wish.isFulfilled()` is called to check whether the wish is completed or not.
.  The updated wish list would be reflected on the UI to be displayed to the user.

The following sequence diagram shows how the Wish Detail Panel displays its updated content:

image::ListCompletedSequenceDiagram.png[width="790"]
// end::list[]

// tag::redesign[]
=== Redesign of User Interface

The UI has been redesigned to implement the following UI components required for WishBook:

* Command Box
* Wish List Panel
* Wish Detail Panel


==== Wish List Panel

The Wish List Panel consists of a list of Wish Card which contains 4 UI elements:

* `WishCard#nameLabel` - A `Text` element that displays the wish’s name.
* `WishCard#progressLabel` - A `Text` element that displays the wish’s saving progress in percentage format.
* `WishCard#tags` - A `FlowPane` element that contains a `Text` element which displays the wish’s assigned tags.
* `WishCard#progressBar` - A `progressBar` element that visually presents the percentage of the wish’s current saving progress.

Whenever the user adds a new wish or edits an existing wish, a new WishCard containing the wish will be added to the Wish List Panel or the content in the existing WishCard will be updated respectively.

The user will be able to view the wish’s current saving progress both in terms of text on the progressLabel (e.g. ’80%’) and the progressBar. Also, the user will be able to see all the tags he/she assigned to categorize the wish.

===== Problem with the old design

The UI (MainWindow) constructs the `WishListPanel` by obtaining an `ObservableList` of wish cards from `Model`, this list is assigned when UI starts, and will never be re-assigned.
The UI "observes" the list and updates when it is modified.

This approach works well for the `WishListPanel` because WishBook contains only 1 list of wish cards.
However, the saving history list in the `WishDetailPanel` can not be updated in the same manner because Model component will change its card list’s reference when a user adds a new wish or updates the content of the wish.

In this case, the `WishDetailPanel` in UI will not be updated because the card list of which UI has reference to is actually not changed.

===== Design considerations

* **Alternative 1 (current choice)**: Have a wishList in `Model` and keep it updated with the current list of cards
** Explanation: The UI needs only 1 reference to this `wishList`, each time user executes any changes, `wishList` is cleared and the new list of cards is copy to the `wishList`.
** Pros: The structure of `Model` and UI component needs not be changed
** Cons: Need to keep a copy of the current card list, copying the whole list of cards for each command operation has bad effect on performance .

* **Alternative 2**: Model component raises an event when its current card list’s reference is changed
** Explanation: When user adds a new wish or executes save, `Model` will raise an event (`WishPanelUpdatedEvent`), which is subscribed by UI, then UI can re-assign its list of cards and update the cards panel accordingly.
** Pros: Better performance
** Cons: Need to re-design `Model` and UI components 

==== Wish Detail Panel

The Wish Detail Panel consists of 3 UI sub-components:

* `WishDetailSavingAmount` that contains `Text` elements to display price and the saving progress of the wish
* `WishDetailSavingHistory` that contains a `List` of history of saving inputs of the wish
* `WishBrowserPanel` that displays `WebView` of the URL of the wish.

Whenever the user adds a new wish or edits an existing wish, the content of the wish in Wish Detail Panel will be updated.
The user will be able to view the wish’s current saving progress and the history of his/her saving inputs of the wish in the list format.
Also, the user will be able to browse through the wish’s product page via its assigned URL.

===== Current Implementation

Every time a new Wish is added or an existing wish is updated by the commands such as save, it raises a `WishDataUpdatedEvent`.
The UI will then handle that event and update the `WishDetailPanel` with the new version of wish.

Given below is an example usage scenario and how the WishBook behaves and `WishDetailPanel` is updated at each step:

.  The user executes the command `save 1 1000`.

[NOTE]
If a command fails its execution, WishDataUpdatedEvent will not be posted.

.  The save command updates the model with the new wish and raises a new `WishDataUpdatedEvent`.
.  `WishDetailSavingAmount` and `WishDetailSavingHistory` responds to the `WishDataUpdatedEvent` with `WishListPanel#handleWishUpdatedEvent()`.
.  The `WishDetailSavingAmount` updates the wish’s current saving progress when `WishDetailSavingAmount#loadWishDetails` is called.
.  The progress is calculated from when `Wish#getProgress` is called. The value is saveAmount / price. Then the progress label for the wish is set to that fraction.
.  The `WishDetailSavingHistory` updates the wish’s saving history list when `WishDetailSavingHistory#loadWishDetails` is called.
.  The saving history list is cleared.
.  The new set of history entry is retrieved from `wishTransaction#getWishMap` and the saved amount is calculated from subtracting previous saving amount from the next one.
.  The saving history list is now filled with the new list of updated saving history.

The following sequence diagram shows how the Wish Detail Panel displays its updated content:

image::WishDetailPanelSequenceDiagram.png[width="790"]

===== Design Considerations

**Aspect: How to update the progress and saving history on UI**

* **Alternative 1 (current choice)**: Clear all the sub components and add new sub components accordingly
** Pros: No matter which progress or history is changed, or what type of change (ie. delete, add, or edit), this change can be handled by the same method each time.
** Cons: It is redundant to clear everything and replace them with new sub components.

* **Alternative 2**: Handle different kinds of changes to the progress or history lists.
** Pros: It is a lot faster to only change the sub component that is affected.
** Cons: There are too many cases for how the lists can be changed. (ie. a different change is needed for each of these cases: wish is deleted/edited/created/cleared, or a `wishTransaction` is deleted/added)

// end::redesign[]

// tag::savingsNotifications[]
=== [Proposed] Savings Notifications
==== Justification
Some users may have many wishes, all of which have a different targeted date of completion and different price.
It may thus be difficult for users to keep track of how much they need to consistently save to fulfil their various
wishes on time.
This Savings Notification feature will allow users to opt for daily/weekly/monthly notifications for each specific wish,
reminding them of the amount that they need to save at the beginning of the chosen time period.
This will help users to consistently save towards their wishes.

// end::savingsNotifications[]

=== Logging

We are using `java.util.logging` package for logging. The `LogsCenter` class is used to manage the logging levels and logging destinations.

* The logging level can be controlled using the `logLevel` setting in the configuration file (See <<Implementation-Configuration>>)
* The `Logger` for a class can be obtained using `LogsCenter.getLogger(Class)` which will log messages according to the specified logging level
* Currently log messages are output through: `Console` and to a `.log` file.

*Logging Levels*

* `SEVERE` : Critical problem detected which may possibly cause the termination of the application
* `WARNING` : Can continue, but with caution
* `INFO` : Information showing the noteworthy actions by the App
* `FINE` : Details that is not usually noteworthy but may be useful in debugging e.g. print the actual list instead of just its size

[[Implementation-Configuration]]
=== Configuration

Certain properties of the application can be controlled (e.g App name, logging level) through the configuration file (default: `config.json`).

== Documentation

We use asciidoc for writing documentation.

[NOTE]
We chose asciidoc over Markdown because asciidoc, although a bit more complex than Markdown, provides more flexibility in formatting.

=== Editing Documentation

See <<UsingGradle#rendering-asciidoc-files, UsingGradle.adoc>> to learn how to render `.adoc` files locally to preview the end result of your edits.
Alternatively, you can download the AsciiDoc plugin for IntelliJ, which allows you to preview the changes you have made to your `.adoc` files in real-time.

=== Publishing Documentation

See <<UsingTravis#deploying-github-pages, UsingTravis.adoc>> to learn how to deploy GitHub Pages using Travis.

=== Converting Documentation to PDF format

We use https://www.google.com/chrome/browser/desktop/[Google Chrome] for converting documentation to PDF format, as Chrome's PDF engine preserves hyperlinks used in webpages.

Here are the steps to convert the project documentation files to PDF format.

.  Follow the instructions in <<UsingGradle#rendering-asciidoc-files, UsingGradle.adoc>> to convert the AsciiDoc files in the `docs/` directory to HTML format.
.  Go to your generated HTML files in the `build/docs` folder, right click on them and select `Open with` -> `Google Chrome`.
.  Within Chrome, click on the `Print` option in Chrome's menu.
.  Set the destination to `Save as PDF`, then click `Save` to save a copy of the file in PDF format. For best results, use the settings indicated in the screenshot below.

.Saving documentation as PDF files in Chrome
image::chrome_save_as_pdf.png[width="300"]

[[Docs-SiteWideDocSettings]]
=== Site-wide Documentation Settings

The link:{repoURL}/build.gradle[`build.gradle`] file specifies some project-specific https://asciidoctor.org/docs/user-manual/#attributes[asciidoc attributes] which affects how all documentation files within this project are rendered.

[TIP]
Attributes left unset in the `build.gradle` file will use their *default value*, if any.

[cols="1,2a,1", options="header"]
.List of site-wide attributes
|===
|Attribute name |Description |Default value

|`site-name`
|The name of the website.
If set, the name will be displayed near the top of the page.
|_not set_

|`site-githuburl`
|URL to the site's repository on https://github.com[GitHub].
Setting this will add a "View on GitHub" link in the navigation bar.
|_not set_

|`site-seedu`
|Define this attribute if the project is an official SE-EDU project.
This will render the SE-EDU navigation bar at the top of the page, and add some SE-EDU-specific navigation items.
|_not set_

|===

[[Docs-PerFileDocSettings]]
=== Per-file Documentation Settings

Each `.adoc` file may also specify some file-specific https://asciidoctor.org/docs/user-manual/#attributes[asciidoc attributes] which affects how the file is rendered.

Asciidoctor's https://asciidoctor.org/docs/user-manual/#builtin-attributes[built-in attributes] may be specified and used as well.

[TIP]
Attributes left unset in `.adoc` files will use their *default value*, if any.

[cols="1,2a,1", options="header"]
.List of per-file attributes, excluding Asciidoctor's built-in attributes
|===
|Attribute name |Description |Default value

|`site-section`
|Site section that the document belongs to.
This will cause the associated item in the navigation bar to be highlighted.
One of: `UserGuide`, `DeveloperGuide`, ``LearningOutcomes``{asterisk}, `AboutUs`, `ContactUs`

_{asterisk} Official SE-EDU projects only_
|_not set_

|`no-site-header`
|Set this attribute to remove the site navigation bar.
|_not set_

|===

=== Site Template

The files in link:{repoURL}/docs/stylesheets[`docs/stylesheets`] are the https://developer.mozilla.org/en-US/docs/Web/CSS[CSS stylesheets] of the site.
You can modify them to change some properties of the site's design.

The files in link:{repoURL}/docs/templates[`docs/templates`] controls the rendering of `.adoc` files into HTML5.
These template files are written in a mixture of https://www.ruby-lang.org[Ruby] and http://slim-lang.com[Slim].

[WARNING]
====
Modifying the template files in link:{repoURL}/docs/templates[`docs/templates`] requires some knowledge and experience with Ruby and Asciidoctor's API.
You should only modify them if you need greater control over the site's layout than what stylesheets can provide.
The SE-EDU team does not provide support for modified template files.
====

[[Testing]]
== Testing

=== Running Tests

There are three ways to run tests.

[TIP]
The most reliable way to run tests is the 3rd one. The first two methods might fail some GUI tests due to platform/resolution-specific idiosyncrasies.

*Method 1: Using IntelliJ JUnit test runner*

* To run all tests, right-click on the `src/test/java` folder and choose `Run 'All Tests'`
* To run a subset of tests, you can right-click on a test package, test class, or a test and choose `Run 'ABC'`

*Method 2: Using Gradle*

* Open a console and run the command `gradlew clean allTests` (Mac/Linux: `./gradlew clean allTests`)

[NOTE]
See <<UsingGradle#, UsingGradle.adoc>> for more info on how to run tests using Gradle.

*Method 3: Using Gradle (headless)*

Thanks to the https://github.com/TestFX/TestFX[TestFX] library we use, our GUI tests can be run in the _headless_ mode. In the headless mode, GUI tests do not show up on the screen. That means the developer can do other things on the Computer while the tests are running.

To run tests in headless mode, open a console and run the command `gradlew clean headless allTests` (Mac/Linux: `./gradlew clean headless allTests`)

=== Types of tests

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.url.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.url.commons.StringUtilTest`
..  _Integration tests_ that are checking the integration of multiple code units (those code units are assumed to be working). +
e.g. `seedu.url.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.url.logic.LogicManagerTest`


=== Troubleshooting Testing
**Problem: `HelpWindowTest` fails with a `NullPointerException`.**

* Reason: One of its dependencies, `HelpWindow.html` in `src/main/resources/docs` is missing.
* Solution: Execute Gradle task `processResources`.

== Dev Ops

=== Build Automation

See <<UsingGradle#, UsingGradle.adoc>> to learn how to use Gradle for build automation.

=== Continuous Integration

We use https://travis-ci.org/[Travis CI] and https://www.appveyor.com/[AppVeyor] to perform _Continuous Integration_ on our projects. See <<UsingTravis#, UsingTravis.adoc>> and <<UsingAppVeyor#, UsingAppVeyor.adoc>> for more details.

=== Coverage Reporting

We use https://coveralls.io/[Coveralls] to track the code coverage of our projects. See <<UsingCoveralls#, UsingCoveralls.adoc>> for more details.

=== Documentation Previews
When a pull request has changes to asciidoc files, you can use https://www.netlify.com/[Netlify] to see a preview of how the HTML version of those asciidoc files will look like when the pull request is merged. See <<UsingNetlify#, UsingNetlify.adoc>> for more details.

=== Making a Release

Here are the steps to create a new release.

.  Update the version number in link:{repoURL}/src/main/java/seedu/url/MainApp.java[`MainApp.java`].
.  Generate a JAR file <<UsingGradle#creating-the-jar-file, using Gradle>>.
.  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, Wish Book depends on the http://wiki.fasterxml.com/JacksonHome[Jackson library] for XML parsing. Managing these _dependencies_ can be automated using Gradle. For example, Gradle can download the dependencies automatically, which is better than these alternatives. +
a. Include those libraries in the repo (this bloats the repo size) +
b. Require developers to download those libraries manually (this creates extra work for developers)

[appendix]
== Product Scope

*Target user profile*:

* has a need to manage savings for a significant number of items to buy
* prefer desktop apps over other types
* can type fast
* prefers typing over mouse input
* is reasonably comfortable using CLI apps

*Value proposition*: manage savings faster than a typical mouse/GUI driven app

[appendix]
== User Stories

Priorities: High (must have) - `* * \*`, Medium (nice to have) - `* \*`, Low (unlikely to have) - `*`

[width="59%",cols="22%,<23%,<25%,<30%",options="header",]
|=======================================================================
|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 wish |keep track of the things I want to purchase

|`* * *` |user |add savings to selected wishes | make faster progress towards certain wishes

|`* * *` |user |delete a wish |remove items that I no longer need

|`* * *` |user |find a wish by name |locate details of a wish without having to go through the entire list

|`* * *` |user |view all fulfilled wishes | so I can keep track of items I have bought

|`* * *` |user |view all past savings for my wishes | have a better idea of my saving habits in general

|`* * *` |user |view all my wishes | monitor the progress I have made in all my wishes

|`* * *` |user |undo past commands | reverse wrong commands

|`* *` |user |distribute a saving to a few wishes | make equal progress to a few of my wishes

|`* *` |user |rank my wishes | prioritise certain wishes over others so that money can be allocated accordingly

|`* *` |user |transfer money from one wish to another | progress towards other wishes faster

|`* *` |user |withdraw from savings | spend the money if need be

|`* *` |user |reorder the priority of a wish | fulfil the specified wish faster

|`* *` |user |save money without a wish | allocate my savings to a wish later

|`*` |user |receive email reminders about wishes that are due |be more mindful of my savings to fulfil wishes

|`*` |user |view all past savings for a particular wish |have a better idea of my saving habits for a wish
|=======================================================================


[appendix]
== Use Cases

(For all use cases below, the *System* is the `WishBook` and the *Actor* is the `user`, unless specified otherwise)

[discrete]
=== Use case: Add wish

*MSS*

1.  *Actor* enters a wish with Name, Date, Price.
2.  *System* adds wish to the wish list.
+
Use case ends.

*Extensions*

[none]
* 2a. *Actor* fails to specify any of the compulsory fields (Name, Price, and Date/Duration).
+
[none]
** 2a1. *System* shows a correct Add command usage with example.
+
Use case ends.

* 2b. *Actor* enters incorrectly formatted arguments.
+
[none]
** 2b1. *System* shows Add command usage.
** 2b2. *Actor* is prompted to enter a valid argument in a specific format shown.
+
Use case ends.

[discrete]
=== Use case: Delete wish

*MSS*

1.  *Actor* requests to list wishes.
2.  *System* shows a list of wishes.
3.  *Actor* requests to delete a specific wish in the list.
4.  *System* deletes the wish.
+
Use case ends.

*Extensions*

[none]
* 2a. The list is empty.
+
Use case ends.

* 3a. The given index is invalid.
+
[none]
** 3a1. *System* shows an error message.
+
Use case resumes at step 2.
** 3b1. Wish requested to be deleted has a non-zero savings amount.
** 3b2. *System* displays warning to user that wish to be deleted has a non-zero savings amount.
+
Use case resumes at step 2.

[discrete]
=== Use case: Edit wish

*MSS*

1.  *Actor* requests to edit wish.
2.  *System* updates wish and shows updated wish to *Actor*.
+
Use case ends.

*Extensions*

[none]
* 1a. *System* has no recorded wishes.
+
[none]
** 1a1. *Actor* is prompted to add a wish.
+
Use case ends.

* 1b. *Actor* enters invalid arguments
+
[none]
** 1b1. *System* shows Edit command usage.
** 1b2. *Actor* is prompted to enter a valid argument.
+
Use case ends.

[discrete]
=== Use case: Find wishes

*MSS*

1.  *Actor* specifies the search predicate.
2.  *System* shows all wishes matching the given search predicate.
+
Use case ends.

*Extensions*

[none]
* 1a. *System* has no recorded wishes.
+
[none]
** 1a1. *System* shows dialog notifying *Actor* that no relevant results can be found.
+
Use case ends.

* 1b. *Actor* enters invalid arguments
+
[none]
** 1b1. *System* shows Find command usage.
** 1b2. *Actor* is prompted to enter a valid argument.
+
Use case ends.

* 1c. *System* unable to find any matching wishes.
+
[none]
** 1c1. *System* shows dialog notifying *Actor* that no relevant results can be found.
+
Use case ends.

* 1d. *Actor* enters empty prefixes for arguments.
+
[none]
** 1c1. *System* shows all wishes in the WishBook.
+
Use case ends.

[discrete]
=== Use case: Move amount between wishes

*MSS*

1.  *Actor* specifies the origin wish index, the destination wish index, and amount to move.
2.  *System* moves specified amount from origin wish to destination wish.
+
Use case ends.

*Extensions*

[none]
* 1a. *System* has no recorded wishes.
+
[none]
** 1a1. *Actor* is prompted to add a wish.
+
Use case ends.

* 1b. *Actor* enters invalid arguments.
+
[none]
** 1b1. *System* shows Move command usage.
** 1b2. *Actor* is prompted to enter a valid argument.
+
Use case ends.

* 1c. *Actor* enters the same index for origin wish and destination wish.
+
[none]
** 1c1. *System* shows Move command usage.
** 1c2. *Actor* is prompted to enter a valid argument.
+
Use case ends.

* 1d. The amount to move is greater than the existing saved amount in the origin wish.
+
[none]
** 1d1. *System* reports that saved amount of wish cannot become negative.
+
Use case ends.

* 1e. The amount moved to destination wish exceeds the amount needed to fulfil the wish.
+
[none]
** 1e1. *System* moves to destination wish the required amount needed to fulfil it.
** 1e2. *System* moves the excess amount to unused funds.
+
Use case ends.

* 1f. *Actor* specifies unused funds as the origin wish.
+
[none]
** 1f1. *System* moves amount from unused funds to the destination wish.
+
Use case ends.

* 1g. *Actor* specifies unused funds as the destination wish.
+
[none]
** 1g1. *System* moves amount from origin wish to unused funds.
+
Use case ends.

[discrete]
=== Use case: Save money for a wish

*MSS*

1.  *Actor* enters [underline]#X# amount of money to be saved.
2.  *System* transfers [underline]#X# to the wish with the specified index.
+
Use case ends.

*Extensions*

[none]
* 1a. *Actor* specifies to allocate the money to unused funds.
+
[none]
** 1a1. *System* adds [underline]#X# to unused funds.
+
Use case ends.

* 1b. *System* has no recorded wishes.
+
[none]
** 1b1. *Actor* is prompted to add a wish.
+
Use case ends.

* 1c. *Actor* enters an invalid value of money to be saved.
+
[none]
** 1c1. *Actor* is prompted to enter a valid value.
+
Use case ends.

* 1d. *Actor* enters an amount of money that causes the wish's saved amount to exceed the amount needed to fulfil the wish.
+
[none]
** 1d1. *System* adds the required amount to fulfil the saved amount of the wish at the specified index.
** 1d2. *System* adds the excess amount to unused funds.
+
Use case ends.

* 1e. *Actor* enters an amount that causes the wish's resulting saved amount to become negative.
+
[none]
** 1e1. *Actor* is prompted to enter a valid value.
+
Use case ends.

[discrete]
=== Use case: View all wishes

*MSS*

1.  *Actor* requests to view all wishes.
2.  *System* shows all wishes.
+
Use case ends.

*Extensions*

[none]
* 1a. *System* has no recorded wishes.
+
[none]
** 1a1. User is prompted to add a wish.
+
Use case ends.

* 1b. *System* has no recorded wishes.
+
[none]
** 1b1. *System* shows dialog notifying *Actor* that there are no such wishes.
+
Use case ends.

[discrete]
=== Use case: View uncompleted wishes

*MSS*

1.  *Actor* requests to view uncompleted wishes.
2.  *System* shows all uncompleted wishes.
+
Use case ends.

*Extensions*

[none]
* 1a. *System* has no recorded uncompleted wishes.
+
[none]
** 1a1. User is prompted to add a wish.
+
Use case ends.

* 1b. *System* has no recorded uncompleted wishes.
+
[none]
** 1b1. *System* shows dialog notifying *Actor* that there are no such wishes.
+
Use case ends.

[discrete]
=== Use case: View completed wish list

*MSS*

1.  *Actor* requests to list completed wishes.
2.  *System* shows a list of completed wishes.
+
Use case ends.

*Extensions*

[none]
* 2a. List is empty.
+
Use case ends.

[discrete]
=== Use case: View command history

*MSS*

1.  *Actor* requests to view history of commands entered.
2.  *System* shows all commands entered.
+
Use case ends.

*Extensions*

[none]
* 1a. *System* has no recorded commands.
+
[none]
** 1a1. User is prompted to enter a command.
+
Use case ends.

[discrete]
=== Use case: View savings history

*MSS*

1.  *Actor* requests to view history of savings entered.
2.  *System* shows all savings entered, from newest to oldest.
+
Use case ends.

*Extensions*

[none]
* 1a. *System* has no recorded savings.
+
[none]
** 1a1. User is prompted to enter a saving.
+
Use case ends.

[appendix]
== Non Functional Requirements

.  Should work on any <<mainstream-os,mainstream OS>> as long as it has Java `9` or higher installed.
.  Should be able to hold up to 1000 wishes without user experiencing a drop in application performance.
.  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.
. User data can be transferred across different machines (of different platforms).
. The software should not use a DBMS (Database Management System) to store data.
. User data is stored locally.
. User data is human readable and can be edited.
. Friendly towards color-blind users.
. Command Line Interface (CLI) is the primary mode of input. GUI is used mainly for visual feedback rather than to collect input. Usage of mouse should be minimized.
. The software should follow the Object-Oriented Paradigm.
. The software should work without requiring an installer.

[appendix]
== Glossary

[[mainstream-os]] Mainstream OS::
Windows, Linux, Unix, OS-X.

[[index]] Index::
Order of priority of a wish.

[[wish]] Wish::
Something the user wants to save up money for.

[[wishlist]] Wishlist::
A record of all wishes added by the user.

[appendix]
== Instructions for Manual Testing

Given below are instructions to test the app manually.

[NOTE]
These instructions only provide a starting point for testers to work on; testers are expected to do more _exploratory_ testing.

=== Launch and Shutdown

. Initial launch

.. Download the jar file and copy into an empty folder
.. Double-click the jar file +
   Expected: Shows the GUI with a set of sample contacts. The window size may not be optimum.

. Saving window preferences

.. Resize the window to an optimum size. Move the window to a different location. Close the window.
.. Re-launch the app by double-clicking the jar file. +
   Expected: The most recent window size and location is retained.

=== Deleting a wish

.  Deleting a wish while all wishes are listed
.. Prerequisites: List all wishes using the list command. Multiple wishes in the list. Wish at index 1 should be fulfilled.
.. Test case: `delete 1` +
   Expected: First wish is deleted from the list. Details of the deleted wish shown in the status message. Unused funds will not be updated as the product has already been bought. Deleted wish details and amount in unused funds shown in status message. Timestamp in the status bar is updated.
.. Test case: `delete 2` +
   Expected: Second wish is deleted. Money in the wish will be transferred to unused funds. Deleted wish details and amount in unused funds shown in status message. Timestamp in the status bar is updated.
.. Test Case: `delete` +
   Expected: Error. Invalid command format message shown in unused funds.
.. Test Case: `delete -1` +
   Expected: Error. Invalid command format message shown in unused funds.
.. Other incorrect delete commands to try: `delete x` (where x is larger than the list size) +
   Expected: Error. Invalid wish provided message in message status.


=== Saving to a wish

. Saving to a wish
.. Prerequisites: Add a new wish with the add command with a date earlier than the earliest wish in the WishBook. If the current date is past the earliest wish’s date, delete the first wish until this is no longer true and then add a new wish earlier than the earliest wish in the WishBook.
.. Test case: `save 1 0` +
   Expected: 0.00 saved to first wish. Details of saved amount and the remaining amount left for completion shown in the status message. Timestamp in the status bar is updated.
.. Test case: `save 1 0.01` +
   Expected: 0.01 saved to first wish. Details of saved amount and the remaining amount left for completion shown in the status message. Timestamp in the status bar is updated.
.. Test case: `save 1 -0.01` +
   Expected: 0.01 removed from first wish. Details of removed amount and the remaining amount left for completion shown in the status message. Timestamp in the status bar is updated.
.. Test case: `save 1 [VALUE LARGER THAN WISH PRICE]` +
   Expected: Wish will be 100% and fulfilled. Details of the excess amount saved and the unused funds value shown in status message.
.. Test case: `save 1 5` +
   Expected: Unable to save to wish. Wish already fulfilled message shown in status message. Timestamp in status bar is updated.
.. Other incorrect delete commands to try: `save 2 0.001`, `save 2 12.`, `save 2 12@` +
   Expected: Similar to previous.

=== Moving funds between wishes
.  Moving funds between 2 wishes:
.. Prerequisites: Wish at index 1 should be fulfilled. If it is not, `save` until it is. The other wishes used in the subsequent commands should not be fulfilled.
.. Test case: `move 1 2 50` +
   Expected: No money moved from wish 1 to 2. Wish already fulfilled status message shown.
.. Test case: `move 2 3 [AMOUNT SMALLER THAN SAVED AMOUNT AT FROM_INDEX]` +
   Expected: Specified amount moved from wish 2 to wish 3. Moved message shown in status message. Timestamp in status bar is updated.
.. Test case: `move 2 3 [AMOUNT LARGER THAN SAVED AMOUNT AT FROM_INDEX]` +
   Expected: No money moved from wish 1 to 2. `FROM_INDEX` does not contain the specified amount shown in status message.
.. Test case: `move 2 3 [AMOUNT LARGER THAN SAVED AMOUNT AT TO_INDEX]` +
   Expected: Money moved from wish 2 to 3. Wish 3 is fulfilled. Excess money goes to unused funds(index 0).  Moved amount, excess and updated unused funds shown in status message. Timestamp in status bar is updated.
   ed. Incorrect move commands to try: `move 2 3 -50`, `move 2 3 0.0001`
   Expected: No money will be moved. Appropriate error message shown in status message.


=== Adding a wish

. Adding to a wish

. Positive test cases
..   Prerequisites: None. Duplicate Wishes can exist and have the same user-defined properties.
..   Test case: `add n/PS4 Golden Pro d/25/12/2018 p/499.99` +
   Expected: A new Wish named “PS4 Golden Pro”, priced at $499.99, and expiry date set to 25th December 2018, default URL of “www.amazon.com” will be added to the list of Wishes. Status message shows the details of the newly added Wish.
.. Test case:  `add n/PS4 Golden Pro d/25/12/2018 p/499.99 t/technology` +
   Expected: A new Wish named “PS4 Golden Pro”, priced at $499.99, and expiry date set to 25th December 2018, default URL of “www.amazon.com”, with a tag named “technology”, will be added to the list of Wishes. Status message shows the details of the newly added Wish. Selecting on the newly added Wish will also bring up the Amazon’s homepage on the right side of the Detail Panel.
.. Test case: `add n/PS4 Golden Pro a/60d p/499.99` +
   Expected: A new Wish named “PS4 Golden Pro”, priced at $499.99, and expiry date set to your current date plus 60 days, default URL of “www.amazon.com” will be added to the list of Wishes. Status message shows the details of the newly added Wish. Selecting on the newly added Wish will also bring up the Amazon’s homepage on the right side of the Detail Panel.
.. Test case: `add n/PS4 Golden Pro a/2y p/499.99 u/www.google.com` +
   Expected: A new Wish named “PS4 Golden Pro”, priced at $499.99, and expiry date set to your current date plus 2 years, default URL of “www.amazon.com” will be added to the list of Wishes. Status message shows the details of the newly added Wish. Selecting on the newly added Wish will also bring up the Google search engine webpage on the right side of the Detail Panel.

.  Negative test cases
.. Prerequisites: None.
.. Test case: `add n/PS’4 Golden Pro a/30d p/499.99` +
   Expected: Status message shows that a Wish’s name can only contain alphanumeric numbers and spaces, and should not be blank.
.. Test case: `add n/ a/30d p/499.99` +
   Expected: Status message shows that a Wish’s name can only contain alphanumeric numbers and spaces, and should not be blank.
.. Test case: `add n/PS4 Golden Pro a/30d3y p/499.99` +
   Expected: Status message shows the given time format is invalid, and also shows the user’s time input.
.. Test case: `add n/PS4 Golden Pro d/30.30.9999 p/499.99` +
   Expected: Status message shows that the time should be of format: dd/mm/yy.
.. Test case: `add n/PS4 Golden Pro a/3d p/499.9999999` +
   Expected: Status message states that price value should only contain numbers, and at most two numbers after the decimal point.
.. Test case: `add n/PS4 Golden Pro a/3d p/abc.de` +
   Expected: Status message states that price value should only contain numbers, and at most two numbers after the decimal point.
.. Test case: `add n/PS4 Golden Pro a/-1d p/22.22` +
   Expected: Status message states that the given expiry date should be after the current date.
.. Test case: `add n/PS4 Golden Pro d/25/12/1111 p/22.22` +
   Expected: Status message states that the given expiry date should be after the current date.
.. Test case: `add n/PS4 Golden Pro a/1d` +
   Expected: Status message states that this is an invalid command format, and shows the correct usage of add command.
.. Test case: `add n/PS4 Golden Pro d/25/12/2018 p/22.22 u/abc def.com` +
   Expected: Status message states that URLs cannot have whitespaces.

=== Listing out wishes

. Listing out wishes in the list
.  Prerequisites: Have both completed (amount saved more than price) and uncompleted wishes.
.. Test case: `list` +
   Expected: All wishes in WishBook is listed.
.. Test case: `list -c` +
   Expected: Only wishes that are completed will be shown on the list.
.. Test case: `list -u` +
   Expected: Only wishes that are uncompleted will be shown on the list.

=== Finding wishes

. Finding a wish by name, tags or remark:

. Prerequisites: None
.. Test case: `find r/Alice` +
Expected: Returns all the wishes whose remark contains “Alice” case insensitive. Number of wishes found is listed in status message. Only matched wishes visible in WishList.
.. Test case: `find t/neigh t/paren` +
Expected: Returns all the wishes whose tags contain the keywords “neigh” or “paren”. Number of wishes found is listed in status message. Only matched wishes visible in WishList.
.. Test case: `find n/EVG t/PS` +
Expected: Returns all wishes whose name contains “EVG” and whose tag contains “PS” case insensitve. Number of wishes found is listed in status message. Only matched wishes visible in WishList.
.. Test case: `find n/EVG n/PS t/neigh t/paren` +
Expected: Returns all wishes whose name contains (“EVG” or “PS”) and  (“neigh” or “paren”). Number of wishes found is listed in status message. Only matched wishes visible in WishList.
.. Test case: `find`  +
Expected: Invalid command. Invalid command format message shown in status message.
.. Other incorrect delete commands to try: `find test`, `find d/22/10/1996`. +
Expected: Similar to previous.


=== Saving data

. Dealing with missing/corrupted data files
.. Prerequisites: content of WishBook.xml and wishTransaction.xml files will be used to load data for a new session of WishBook. If data is missing from any of these files, data will be lost from subsequent sessions of WishBook unless the xml file data is restored and in proper format.
.. Test case (gracefully exiting app assuming that WishBook contains more than 1 wish): `save 1 10` and then `exit` +
Expected: when a new session is started, WishBook should contain all wishes from the previous session. Wish in which the save command is performed for should have its saving history and save amount updated.
.. Test case (WishBook data and wishTransaction data should be backed up everytime the state of WishBook changes): `save 1 10` +
Expected: assuming WishBook contains more than 1 wish, the execution of the save command should cause the WishBook.xml and wishTransaction.xml files to be updated with a new save amount.
.. Test case (empty WishBook to simulate missing data) Copy the contents of the empty xml file after executing the clear command to simulate missing file data into WishBook.xml at the file path specified in the save location (which is specified in the preferences.json file) +
Expected: When the WishBook is loaded, all wish data should be absent.
.. Test case (corrupted data): delete data for 1 wish currently in WishBook by editing the WishBook.xml file. +
Expected: assuming that the data format of the xml file is valid, WishBook should load the content of the WishBook based on the data present in WishBook.xml file (without the deleted wish data). If the data format of the xml file is invalid the WishBook will not load properly and an error message should appear.
<file_sep>/src/test/java/seedu/address/testutil/TypicalWishes.java
package seedu.address.testutil;

import static seedu.address.logic.commands.CommandTestUtil.VALID_DATE_1;
import static seedu.address.logic.commands.CommandTestUtil.VALID_DATE_2;
import static seedu.address.logic.commands.CommandTestUtil.VALID_DATE_CHARLES;
import static seedu.address.logic.commands.CommandTestUtil.VALID_ID_AMY;
import static seedu.address.logic.commands.CommandTestUtil.VALID_ID_BOB;
import static seedu.address.logic.commands.CommandTestUtil.VALID_ID_CHARLES;
import static seedu.address.logic.commands.CommandTestUtil.VALID_NAME_AMY;
import static seedu.address.logic.commands.CommandTestUtil.VALID_NAME_BOB;
import static seedu.address.logic.commands.CommandTestUtil.VALID_NAME_CHARLES;
import static seedu.address.logic.commands.CommandTestUtil.VALID_PRICE_AMY;
import static seedu.address.logic.commands.CommandTestUtil.VALID_PRICE_BOB;
import static seedu.address.logic.commands.CommandTestUtil.VALID_PRICE_CHARLES;
import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_FRIEND;
import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_HUSBAND;
import static seedu.address.logic.commands.CommandTestUtil.VALID_URL_AMY;
import static seedu.address.logic.commands.CommandTestUtil.VALID_URL_BOB;
import static seedu.address.logic.commands.CommandTestUtil.VALID_URL_CHARLES;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import seedu.address.model.WishBook;
import seedu.address.model.WishTransaction;
import seedu.address.model.wish.Wish;

/**
 * A utility class containing a list of {@code Wish} objects to be used in tests.
 */
public class TypicalWishes {
    /* Following wishes must have unique names and must be in ascending dates. Or else the
     * following tests will fail:
     * EditCommandTest#execute_duplicateWishFilteredList_failure
     * CommandTestUtil#showWishAtIndex
     */
    public static final Wish ALICE = new WishBuilder().withName("<NAME>")
            .withUrl("https://www.lazada.sg/products/"
                    + "ps4-092-hori-real-arcade-pron-hayabusaps4ps3pc-i223784444-s340908955.html")
            .withDate("4/09/2020")
            .withPrice("94.35")
            .withTags("friends")
            .withSavedAmountIncrement("0.00")
            .withId("bf557b11-6b8d-4aa0-83cb-b730253bb6e2")
            .build();
    public static final Wish BENSON = new WishBuilder().withName("<NAME>")
            .withUrl("https://www.amazon.com/Apple-iPhone-Fully-Unlocked-32GB/dp/B0731HBTZ7")
            .withDate("21/09/2020")
            .withPrice("98.76")
            .withTags("owesMoney", "friends")
            .withSavedAmountIncrement("0.00")
            .withId("b38ceb7a-2cd3-48c9-860c-3918155e4fd6")
            .build();
    public static final Wish CARL = new WishBuilder().withName("<NAME>")
            .withPrice("98.76")
            .withDate("21/09/2020")
            .withUrl("https://www.amazon.com/EVGA-GeForce-Gaming-GDDR5X-Technology/dp/B0762Q49NV")
            .withSavedAmountIncrement("0.00")
            .withId("a758eec0-6056-4737-9c05-d3bf60f6f67d")
            .build();
    public static final Wish DANIEL = new WishBuilder().withName("<NAME>")
            .withPrice("87.65")
            .withDate("21/11/2023")
            .withUrl("https://www.amazon.com/Logitech-Mechanical-Keyboard-Romer-Switches/dp/B071VHYZ62")
            .withTags("friends")
            .withSavedAmountIncrement("0.00")
            .withId("a2dde843-9cde-4417-946d-40bc465462cf")
            .build();
    public static final Wish ELLE = new WishBuilder().withName("<NAME>")
            .withPrice("94.82")
            .withDate("19/07/2020")
            .withUrl("https://www.amazon.com/EVGA-GeForce-Gaming-GDDR5X-Technology/dp/B0762Q49NV")
            .withSavedAmountIncrement("0.00")
            .withId("6387ec7d-da35-4569-a549-a8332e650429")
            .build();
    public static final Wish FIONA = new WishBuilder().withName("<NAME>")
            .withPrice("94.82")
            .withDate("24/09/2010")
            .withUrl("https://www.lazada.sg/products/"
                    + "nintendo-switch-neon-console-1-year-local-warranty-best-seller-i180040203-s230048296.html")
            .withSavedAmountIncrement("0.00")
            .withId("dcb605d6-af12-444d-a2ee-44b5fccfe137")
            .build();
    public static final Wish GEORGE = new WishBuilder().withName("<NAME>")
            .withPrice("94.82")
            .withDate("24/09/2019")
            .withUrl("https://www.amazon.com/EVGA-GeForce-Gaming-GDDR5X-Technology/dp/B0762Q49NV")
            .withSavedAmountIncrement("0.00")
            .withId("32ea1ae8-f1da-435c-a6a8-ca501a9650c1")
            .build();

    // Manually added
    public static final Wish HOON = new WishBuilder().withName("<NAME>")
            .withPrice("84.82")
            .withDate("28/08/2019")
            .withUrl("https://www.amazon.com/EVGA-GeForce-Gaming-GDDR5X-Technology/dp/B0762Q49NV")
            .withSavedAmountIncrement("0.00")
            .withId("ace52214-c1d5-4dff-a05c-b0e6ac1c5fa9")
            .build();
    public static final Wish IDA = new WishBuilder().withName("<NAME>")
            .withPrice("84.82")
            .withDate("02/09/2019")
            .withUrl("https://www.amazon.com/EVGA-GeForce-Gaming-GDDR5X-Technology/dp/B0762Q49NV")
            .withSavedAmountIncrement("0.00")
            .withId("c8e1624c-9d91-4c3d-85e1-dee255126944")
            .build();

    // Manually added - Wish's details found in {@code CommandTestUtil}
    public static final Wish AMY = new WishBuilder().withName(VALID_NAME_AMY).withPrice(VALID_PRICE_AMY)
            .withDate(VALID_DATE_1).withUrl(VALID_URL_AMY).withTags(VALID_TAG_FRIEND)
            .withSavedAmountIncrement("0.00")
            .withId(VALID_ID_AMY)
            .build();
    public static final Wish BOB = new WishBuilder().withName(VALID_NAME_BOB).withPrice(VALID_PRICE_BOB)
            .withDate(VALID_DATE_2).withUrl(VALID_URL_BOB)
            .withTags(VALID_TAG_HUSBAND, VALID_TAG_FRIEND).withSavedAmountIncrement("0.00")
            .withId(VALID_ID_BOB)
            .build();
    public static final Wish CHARLES = new WishBuilder().withName(VALID_NAME_CHARLES).withPrice(VALID_PRICE_CHARLES)
            .withDate(VALID_DATE_CHARLES).withUrl(VALID_URL_CHARLES)
            .withId(VALID_ID_CHARLES)
            .build();

    public static final String KEYWORD_MATCHING_MEIER = "Meier"; // A keyword that matches MEIER

    private TypicalWishes() {} // prevents instantiation1

    /**
     * Returns an {@code WishBook} with all the typical wishes.
     */
    public static WishBook getTypicalWishBook() {
        WishBook ab = new WishBook();
        for (Wish wish : getTypicalWishes()) {
            ab.addWish(wish);
        }
        return ab;
    }

    /**
     * Returns a {@code WishTransaction} with all the typical wish transactions.
     * Assumption: no previous wish transaction available other than current state of {@code WishBook}.
     */
    public static WishTransaction getTypicalWishTransaction() {
        return new WishTransaction(getTypicalWishBook());
    }

    public static List<Wish> getTypicalWishes() {
        return new ArrayList<>(Arrays.asList(ALICE, BENSON, CARL, DANIEL, ELLE, FIONA, GEORGE));
    }
}
<file_sep>/src/main/java/seedu/address/model/wish/WishCompletedPredicate.java
package seedu.address.model.wish;

import java.util.function.Predicate;

/**
 * Tests if a {@code Wish} is completed.
 */
public class WishCompletedPredicate implements Predicate<Wish> {
    private final boolean isCompletedList;

    public WishCompletedPredicate(boolean isCompleted) {
        this.isCompletedList = isCompleted;
    }

    @Override
    public boolean test(Wish wish) {
        return wish.isFulfilled() == this.isCompletedList;
    }

    @Override
    public boolean equals(Object other) {
        return other == this // short circuit if same object
                || (other instanceof WishCompletedPredicate // instanceof handles nulls
                && isCompletedList == (((WishCompletedPredicate) other).isCompletedList)); // state check
    }
}
<file_sep>/docs/team/lionelat.adoc
= <NAME> - Project Portfolio
:site-section: AboutUs
:imagesDir: ../images
:stylesDir: ../stylesheets

== PROJECT: WishBook

---

== Overview

WishBook is a desktop savings assistant application written in Java, built for people who want to set savings goals (Wishes)
and keep track of their progress in saving toward those wishes.
It is created by Team T16-1 for CS2103T: Software Engineering module offered by School of Computing,
National University of Singapore, in AY2018/2019 Semester 1.
The image below shows the main layout of our application:

image::Ui.png[width="800"]

WishBook aims to promote responsible saving and spending among users, by helping them be more prudent with their income
and encouraging regular, incremental savings to attain their wishes. The source code of WishBook can be found
https://github.com/CS2103-AY1819S1-T16-1/main[here].

Being in charge of the Logic Component and documentation, I was tasked with implementing new commands, while also making sure
that all changes made by our team were consistently documented in our User Guide and Developer Guide.
This project portfolio consists of the contributions I have made to WishBook over the semester.


== Summary of contributions
|===
|_Given below are the contributions I have made to WishBook's development. Major components were delegated to each member
of the team, and I was tasked with the Logic component._
|===

=== Major Enhancements
* *Move Command*: added Move Command
** *What it does*: Allows users to move funds between wishes, or to transfer between wishes and the unused funds.
** *Justification*: Previously, to reallocate funds from one wish to another, the user had to save a negated amount from one of their wishes,
and then save that positive amount into the targeted wish.
It can be quite troublesome to shift funds between wishes, as the user would have to type two separate commands.
As such, I developed the move command that allows the user to transfer funds between wishes directly in a single command.

* *Save to unused funds*:
** *What it does*: Allow users to save toward unused funds directly. Also, any excess saved amount will be channelled automatically to unused funds.
** *Justification*: Previously, if the input amount causes the wish’s saved amount to exceed its price,
the excess amount would be stored inside that wish.
However, this excess amount cannot be reallocated, since the wish has already been fulfilled.
Thus, I modified the Save command to channel any excess saved amount to an UnusedFunds within WishBook.
The money inside the UnusedFunds can later be redistributed to other wishes, via the Move Command.

=== Minor Enhancements
* *Savings History*: add option to show savings history for History Command.

* *List*: add option to display only completed or uncompleted wishes.

=== Other Contributions
** Shorthand alias: Add shorthand equivalent alias for commands.
*** Useful to increase speed and efficiency while using WishBook
*** users would now be able to type a command with only 1 or 2 characters, instead of typing the whole word.
(Pull Request https://github.com/CS2103-AY1819S1-T16-1/main/pull/3[#3])
** Documentation:
*** Documented new functions added to User Guide.
*** Consistently updated the User Guide to reflect the current function usage in our application.
*** Refactored Developer Guide to make it specific to our WishBook implementation.

** Refactored code and documentation:
*** Contributed regularly to refactoring the code from the previous AddressBook-Level 4 application.
*** Refactor terms specific to AddressBook-Level 4, to suit our current WishBook application

** Reported bugs and issues:
https://github.com/CS2103-AY1819S1-T16-1/main/issues/61[#61]
https://github.com/CS2103-AY1819S1-T16-1/main/issues/created_by/lionelat[#90]

*Code contributed*: https://nus-cs2103-ay1819s1.github.io/cs2103-dashboard/#=undefined&search=lionelat[Project Code DashBoard Link]

== 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._
|===

=== Save money for a particular wish: `save`

Channel a specified amount of money to savings for a specified wish. +
Format: `save INDEX AMOUNT`

****
* `INDEX` should be a positive integer 1, 2, 3… no larger than the number of wishes.
* If `INDEX` is 0, `AMOUNT` will be channelled directly to `unusedFunds`.
* If `AMOUNT` saved to `INDEX` is greater than the amount needed to fulfil that wish, excess funds will be channelled
to `unusedFunds`.
* If `AMOUNT` is negative, money will be removed from amount saved for that wish. +
* `AMOUNT` will not be accepted if:
** `AMOUNT` brings the savings value for that wish to below 0.
** The wish at `INDEX` is already fulfilled.
****

Examples: +

* `save 1 1000` +
Attempt to save $1000 for the wish at index 1.

* `save 1 -100.50` +
Attempt to remove $100.50 from the savings for the wish at index 1.

* `save 0 100.50` +
Attempt save $100.50 to `unusedFunds`.

=== Move money between wishes: `move`

Moves funds from one wish to another. +
Format: `move FROM_WISH_INDEX TO_WISH_INDEX AMOUNT`

****
* `FROM_WISH_INDEX` and `TO_WISH_INDEX` should be a positive integer 1, 2, 3… no larger than the number of wishes
* If `FROM_WISH_INDEX` is 0, `AMOUNT` will be channelled from `unusedFunds` to `TO_WISH_INDEX`.
* If `TO_WISH_INDEX` is 0, `AMOUNT` will be channelled from `FROM_WISH_INDEX` to `unusedFunds`.
* `AMOUNT` from `unusedFunds` will only be successfully channelled if the exact amount requested is present in `unusedFunds`.
* If `FROM_WISH_INDEX` equals `TO_WISH_INDEX`, both indexes will not be accepted.
* If `AMOUNT` saved to `TO_WISH_INDEX` is greater than the amount needed to fulfil that wish, excess funds will be channelled to `unusedFunds`.
* `AMOUNT` will not be accepted if:
** `AMOUNT` is negative.
** `AMOUNT` brings the savings amount of wish at `FROM_WISH_INDEX` to below 0.
** Either wish at `FROM_WISH_INDEX` or `TO_WISH_INDEX` is already fulfilled.
****

[NOTE]
====
Index 0 is specially allocated for `unused funds`. Excess funds when user attempts to save to a wish will be
automatically allocated to `unusedFunds`.
The user can also choose to channel funds from `unusedFunds` to a valid wish.
====

Examples: +

* `move 1 2 10` +
Attempt to move $10 from the wish at index 1 to the wish at index 2.

* `move 0 1 10` +
Attempt to move $10 from `unusedFunds` to the wish at index 1.

* `move 1 0 10` +
Attempt to move $10 from the wish at index 1 to `unusedFunds`.

//include::../UserGuide.adoc[tag=save]

//include::../UserGuide.adoc[tag=move]

== 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._
|===

=== Save Amount feature

==== Current Implementation

The Save Amount feature is executed through a `SaveCommand` by the user, which after parsing,
is facilitated mainly by the `ModelManager` which implements `Model`.
Wish stores the `price` and `savedAmount` of `Wish`, helping to track the progress of the savings towards the `price`.
Meanwhile, WishBook stores an `unusedFunds`, which is an unallocated pool of funds that can be used in the future.
After adding a saving, the `filteredSortedWishes` in `ModelManager` is updated to reflect the latest observable WishBook.

Given below is an example usage scenario and how the SaveCommand behaves at each step:

Step 1. The user executes `save 1 10`, to save $10 into an existing wish with `Index` 1 and `Price` $15. The $10 is
wrapped in an `Amount` and a `SaveCommand` instance is created with the `Amount`. `Amount` is then used to make an updated
instance of the `Wish` at index 1 whose `SavedAmount` will be updated. `Model#updateWish` is then called to update this
wish with the old one in `WishBook`.

[NOTE]
The `Index` of each `Wish` is labelled at the side of the app.

The resultant wish will have the following properties:

* Name: _1TB Toshiba SSD_
* SavedAmount: 10.00
* Price: 15.00
* Date: 20/4/2018 (20th April 2018)
* URL: `empty string`
* Remark: `empty string`
* Tags: `none`
* Fulfilled: `false`
* Expired: `false`

[NOTE]
`Amount` to be saved can be a negative value where it would mean a withdrawal of money from a particular wish.

[NOTE]
`SavedAmount` of a wish cannot be negative. This means that an `Amount` cannot be negative enough to cause `SavedAmount`
to be negative.

Step 2. The user decides to execute `save 1 10` again. However, SaveCommand checks that `savedAmount` > `price`.
SaveCommand#execute creates a new updated `Wish` with `savedAmount = wish.getPrice()`.

The resultant wish will have the following properties:

* Name: _1TB Toshiba SSD_
* SavedAmount: 15.00
* Price: 15.00
* Date: 20/4/2018 (20th April 2018)
* URL: `empty string`
* Remark: `empty string`
* Tags: `none`
* Fulfilled: `true`
* Expired: `false`

Step 3. The excess amount of $5 is stored in a new `Amount` variable `excess`.
SaveCommand#execute then calls Model#updateUnusedFunds(excess) to update the `unusedFunds` in WishBook.

In WishBook, the result would be:

* unusedFunds: 5.00

Step 4. The user tries to execute `save 1 10` again. However, since the value for Wish#isFulfilled is true, the amount
will not be saved. SaveCommand#execute will throw a CommandException, with the message "Wish has already been fulfilled!".

The following sequence diagram shows how the save operation works:

image::SaveCommandSequenceDiagram.png[width="800"]

==== Design Considerations
===== Aspect: Data structure to support the unusedFunds feature

* **Alternative 1 (current choice):** Store it in a `SavedAmount` variable in `WishBook`.
** Pros: Easy to implement.
** Cons: More methods needed when needing to move funds from `unusedFunds` to other wishes.
* **Alternative 2:** Store it as a pseudo wish with index 0.
** Pros: It can be treated as another `wish`, hence existing methods can be used without needing to create much more new ones.
** Cons: Requires dealing with an extra wish that has to be hidden on the `WishListPanel` and displayed separately on the UI.
We must remember to skip this wish in methods that involve displaying the WishList.

=== [Proposed] Savings Notifications
==== Justification
Some users may have many wishes, all of which have a different targeted date of completion and different price.
It may thus be difficult for users to keep track of how much they need to consistently save to fulfil their various wishes on time.
This Savings Notification feature will allow users to opt for daily/weekly/monthly notifications for each specific wish,
reminding them of the amount that they need to save at the beginning of the chosen time period.
This will help users to consistently save towards their wishes.

//include::../DeveloperGuide.adoc[tag=save]

//include::../DeveloperGuide.adoc[tag=savingsNotifications]


---

<file_sep>/docs/team/leongshengmin.adoc
= <NAME> - Project Portfolio
:site-section: AboutUs
:imagesDir: ../images
:stylesDir: ../stylesheets

== PROJECT: WishBook
By: `CS2103T Team T16-1`

---

== Overview

WishBook (WB) is a piggy bank in a digital app form, made for people who need a user-friendly solution for keeping track of their disposable income. Not only does WB keep records of your savings, it also allows users to set items as goals for users to work towards, serving as a way to cultivate the habit of saving. WB is made for people who have material wants in life, but are uncertain of purchasing said wants, by helping them to reserve disposable income so that they know they can spend on their wants.

This project portfolio consists of the contributions I have made to WishBook over the semester.
****
Below is a mockup of `WishBook`

image::Ui.png[width="790", caption="Fig 1: Mockup of `WishBook`", align="center"]
_Fig 1: Mockup of WishBook_
****
<<<

== Summary of contributions

=== *Major enhancement*: added the ability to view savings history

****
Below is a screenshot of the savings history for a `Wish`

image::savingsHistoryUI.png[caption="Fig 2: Savings History of a Wish", align="center", width="300"]
_Fig 2: Savings History of a Wish_
****
* What it does: Shows a history of savings you have allocated for any particular wish in the wishbook, from newest to oldest.
* Justification: This feature improves the product significantly because the automatic book-keeping of savings for each Wish allows the user to easily and clearly keep track of his or her savings over a length of time.
+
This feature also allows for extensions since the data collected in terms of amount saved as well as frequency of saving allows for precious insights to be drawn about the saving habits as well as saving priorities of the user.
* Highlights: This enhancement affects existing commands and commands to be added in future.
+
The implementation of this feature involves many different components and is intrinsically tied to the state of the `WishBook`, which changes when the user executes an action-based command such as the `Add`, `Save`, `Edit` command.

<<<

====
Areas involved:

* Persistent Storage
** Facilitate reading and writing of savings histories of all wishes to hard disk. This data will also be saved to the hard disk when triggered by a change in the state of the wishbook.

* Dynamic updating of savings history
** In order for savings histories of each `Wish` to be updated dynamically, the implementation follows an event-driven model. When an action-based command is executed, subscribed classes will be notified of the change in state of the `WishBook`.
** The implementation of tracking the savings history of each `Wish` also needs to follow a `VersionedModel` that responds when notified by the change in `WishBook` state.
* Testing
** Unit and Integrated testing for the code contributed.
====

* Credits:
** Persistent storage
*** Savings history saved as `xml` format using the `JAXB` framework.
** Dynamic updating of savings history
*** Follows event-driven model of `AddressBook-L4`
*** `VersionedModel` implementation inspired by `undo`, `redo` functionality in `AddressBook-L4`.
** Testing
*** `JUnit` testing framework adopted.

<<<

=== *Minor enhancement*: updated the  `Help Window` display.
The `Help Window` is a pop-up window that appears when the `Help` command is executed. It is a guide to aid the user navigate the `WishBook`.
****
The image below shows a screenshot of the `Help Window`.

image::helpWindow.png[width="790", caption="Fig 3: Help Window", align="center"]
_Fig 3: Help Window_
****

=== *Code contributed*:
_The following links contain code snippets of code contributed_ [https://github.com/CS2103-AY1819S1-T16-1/main/commit/6fb424a707d9bee9bd5b2441c741c5051bed3b18#diff-7b7c12031dc90ab5f6a8ec163dc2e541[Functional code]]  [https://github.com/CS2103-AY1819S1-T16-1/main/commit/6fb424a707d9bee9bd5b2441c741c5051bed3b18#diff-b84c42f13e31e9508d50fbccaca0ac08[Test code]]

=== *Other contributions*:
* Documentation:
** User Guide:
*** Wrote the first version of the user guide containing all features in the first version of `WishBook`.[https://github.com/CS2103-AY1819S1-T16-1/main/blob/master/docs/old_userguide.md[link]]
*** Added content for `History -s` command. [https://github.com/CS2103-AY1819S1-T16-1/main/blob/master/docs/UserGuide.adoc#viewing-your-saving-history-code-history-s-code[link]]
** Developer Guide:
*** Added content for `History -s` command. [https://github.com/CS2103-AY1819S1-T16-1/main/blob/master/docs/DeveloperGuide.adoc#savings-history-feature[link]]
*** Refactored undo, redo UML diagrams to reflect current version of WishBook.
****
The following images show the undo, redo UML diagrams:

image::UndoRedoSequenceDiagram.png[caption="Fig 5.1: Undo Redo Sequence Diagram", align="center"]
_Fig 5.1: Undo Redo Sequence Diagram_

image::UndoRedoExecuteUndoStateListDiagram.png[caption="Fig 5.2.2 Undo Redo State Diagram after `Undo` command executed", align="center"]
_Fig 5.2.2 Undo Redo State Diagram after `Undo` command executed_

image::UndoRedoActivityDiagram.png[caption="Fig 5.3 Undo Redo Activity Diagram", align="center"]
_Fig 5.3 Undo Redo Activity Diagram_
****
<<<

** Updated the Model class diagram.
****
The following image shows the updated Model Class Diagram.

image::ModelClassDiagram.png[caption="Fig 6: Model Class Diagram", align="center"]
_Fig 6: Model Class Diagram_
****
** Updated the Storage class diagram.
****
The following image shows the updated Storage Class Diagram.

image::StorageClassDiagram.png[width="790", caption="Fig 7: Storage Class Diagram", align="center"]
_Fig 7: Storage Class Diagram_
****
* Community:
** PRs reviewed (with non-trivial review comments): [https://github.com/CS2103-AY1819S1-T16-1/main/pull/54[#54]], [https://github.com/CS2103-AY1819S1-T16-1/main/pull/32[#32]]
** Reported bugs and suggestions for other teams in the class [https://github.com/CS2103-AY1819S1-W10-2/main/issues/285[1]], [https://github.com/CS2103-AY1819S1-W10-2/main/issues/276[2]], [https://github.com/CS2103-AY1819S1-W10-2/main/issues/275[3]]
** Raised and resolved issues [https://github.com/CS2103-AY1819S1-T16-1/main/issues/created_by/leongshengmin[1]], [https://github.com/CS2103-AY1819S1-T16-1/main/issues?q=is%3Aissue+assignee%3Aleongshengmin+is%3Aclosed[2]]

<<<

* Tools:
** Configured `RepoSense`, a Contribution Analysis Tool for the team repo [https://github.com/CS2103-AY1819S1-T16-1/main/pull/62/files[#62]]

== 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=savingsHistory]

== 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[caption="Savings History", tag=savingsHistory]


|===
|_Proposed enhancement for V2.0_
|===

include::../DeveloperGuide.adoc[tag=savingsNotifications, caption="Savings Notifications"]

---
<file_sep>/src/test/java/seedu/address/model/VersionedWishBookTest.java
package seedu.address.model;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static seedu.address.testutil.TypicalWishes.AMY;
import static seedu.address.testutil.TypicalWishes.BOB;
import static seedu.address.testutil.TypicalWishes.CARL;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import org.junit.Test;

import seedu.address.model.versionedmodels.VersionedWishBook;
import seedu.address.testutil.WishBookBuilder;

public class VersionedWishBookTest {

    private final ReadOnlyWishBook wishBookWithAmy = new WishBookBuilder().withWish(AMY).build();
    private final ReadOnlyWishBook wishBookWithBob = new WishBookBuilder().withWish(BOB).build();
    private final ReadOnlyWishBook wishBookWithCarl = new WishBookBuilder().withWish(CARL).build();
    private final ReadOnlyWishBook emptyWishBook = new WishBookBuilder().build();

    @Test
    public void commit_singleWishBook_noStatesRemovedCurrentStateSaved() {
        VersionedWishBook versionedWishBook = prepareWishBookList(emptyWishBook);
        versionedWishBook.commit();
        assertWishBookListStatus(versionedWishBook,
                Collections.singletonList(emptyWishBook),
                emptyWishBook,
                Collections.emptyList());
    }

    @Test
    public void commit_multipleWishBookPointerAtEndOfStateList_noStatesRemovedCurrentStateSaved() {
        VersionedWishBook versionedWishBook = prepareWishBookList(
                emptyWishBook, wishBookWithAmy, wishBookWithBob);

        versionedWishBook.commit();
        assertWishBookListStatus(versionedWishBook,
                Arrays.asList(emptyWishBook, wishBookWithAmy, wishBookWithBob),
                wishBookWithBob,
                Collections.emptyList());
    }

    @Test
    public void commit_multipleWishBookPointerNotAtEndOfStateList_statesAfterPointerRemovedCurrentStateSaved() {
        VersionedWishBook versionedWishBook = prepareWishBookList(
                emptyWishBook, wishBookWithAmy, wishBookWithBob);
        shiftCurrentStatePointerLeftwards(versionedWishBook, 2);

        versionedWishBook.commit();
        assertWishBookListStatus(versionedWishBook,
                Collections.singletonList(emptyWishBook),
                emptyWishBook,
                Collections.emptyList());
    }

    @Test
    public void canUndo_multipleWishBookPointerAtEndOfStateList_returnsTrue() {
        VersionedWishBook versionedWishBook = prepareWishBookList(
                emptyWishBook, wishBookWithAmy, wishBookWithBob);

        assertTrue(versionedWishBook.canUndo());
    }

    @Test
    public void canUndo_multipleWishBookPointerAtStartOfStateList_returnsTrue() {
        VersionedWishBook versionedWishBook = prepareWishBookList(
                emptyWishBook, wishBookWithAmy, wishBookWithBob);
        shiftCurrentStatePointerLeftwards(versionedWishBook, 1);

        assertTrue(versionedWishBook.canUndo());
    }

    @Test
    public void canUndo_singleWishBook_returnsFalse() {
        VersionedWishBook versionedWishBook = prepareWishBookList(emptyWishBook);

        assertFalse(versionedWishBook.canUndo());
    }

    @Test
    public void canUndo_multipleWishBookPointerAtStartOfStateList_returnsFalse() {
        VersionedWishBook versionedWishBook = prepareWishBookList(
                emptyWishBook, wishBookWithAmy, wishBookWithBob);
        shiftCurrentStatePointerLeftwards(versionedWishBook, 2);

        assertFalse(versionedWishBook.canUndo());
    }

    @Test
    public void canRedo_multipleWishBookPointerNotAtEndOfStateList_returnsTrue() {
        VersionedWishBook versionedWishBook = prepareWishBookList(
                emptyWishBook, wishBookWithAmy, wishBookWithBob);
        shiftCurrentStatePointerLeftwards(versionedWishBook, 1);

        assertTrue(versionedWishBook.canRedo());
    }

    @Test
    public void canRedo_multipleWishBookPointerAtStartOfStateList_returnsTrue() {
        VersionedWishBook versionedWishBook = prepareWishBookList(
                emptyWishBook, wishBookWithAmy, wishBookWithBob);
        shiftCurrentStatePointerLeftwards(versionedWishBook, 2);

        assertTrue(versionedWishBook.canRedo());
    }

    @Test
    public void canRedo_singleWishBook_returnsFalse() {
        VersionedWishBook versionedWishBook = prepareWishBookList(emptyWishBook);

        assertFalse(versionedWishBook.canRedo());
    }

    @Test
    public void canRedo_multipleWishBookPointerAtEndOfStateList_returnsFalse() {
        VersionedWishBook versionedWishBook = prepareWishBookList(
                emptyWishBook, wishBookWithAmy, wishBookWithBob);

        assertFalse(versionedWishBook.canRedo());
    }

    @Test
    public void undo_multipleWishBookPointerAtEndOfStateList_success() {
        VersionedWishBook versionedWishBook = prepareWishBookList(
                emptyWishBook, wishBookWithAmy, wishBookWithBob);

        versionedWishBook.undo();
        assertWishBookListStatus(versionedWishBook,
                Collections.singletonList(emptyWishBook),
                wishBookWithAmy,
                Collections.singletonList(wishBookWithBob));
    }

    @Test
    public void undo_multipleWishBookPointerNotAtStartOfStateList_success() {
        VersionedWishBook versionedWishBook = prepareWishBookList(
                emptyWishBook, wishBookWithAmy, wishBookWithBob);
        shiftCurrentStatePointerLeftwards(versionedWishBook, 1);

        versionedWishBook.undo();
        assertWishBookListStatus(versionedWishBook,
                Collections.emptyList(),
                emptyWishBook,
                Arrays.asList(wishBookWithAmy, wishBookWithBob));
    }

    @Test
    public void undo_singleWishBook_throwsNoUndoableStateException() {
        VersionedWishBook versionedWishBook = prepareWishBookList(emptyWishBook);

        assertThrows(VersionedWishBook.NoUndoableStateException.class, versionedWishBook::undo);
    }

    @Test
    public void undo_multipleWishBookPointerAtStartOfStateList_throwsNoUndoableStateException() {
        VersionedWishBook versionedWishBook = prepareWishBookList(
                emptyWishBook, wishBookWithAmy, wishBookWithBob);
        shiftCurrentStatePointerLeftwards(versionedWishBook, 2);

        assertThrows(VersionedWishBook.NoUndoableStateException.class, versionedWishBook::undo);
    }

    @Test
    public void redo_multipleWishBookPointerNotAtEndOfStateList_success() {
        VersionedWishBook versionedWishBook = prepareWishBookList(
                emptyWishBook, wishBookWithAmy, wishBookWithBob);
        shiftCurrentStatePointerLeftwards(versionedWishBook, 1);

        versionedWishBook.redo();
        assertWishBookListStatus(versionedWishBook,
                Arrays.asList(emptyWishBook, wishBookWithAmy),
                wishBookWithBob,
                Collections.emptyList());
    }

    @Test
    public void redo_multipleWishBookPointerAtStartOfStateList_success() {
        VersionedWishBook versionedWishBook = prepareWishBookList(
                emptyWishBook, wishBookWithAmy, wishBookWithBob);
        shiftCurrentStatePointerLeftwards(versionedWishBook, 2);

        versionedWishBook.redo();
        assertWishBookListStatus(versionedWishBook,
                Collections.singletonList(emptyWishBook),
                wishBookWithAmy,
                Collections.singletonList(wishBookWithBob));
    }

    @Test
    public void redo_singleWishBook_throwsNoRedoableStateException() {
        VersionedWishBook versionedWishBook = prepareWishBookList(emptyWishBook);

        assertThrows(VersionedWishBook.NoRedoableStateException.class, versionedWishBook::redo);
    }

    @Test
    public void redo_multipleWishBookPointerAtEndOfStateList_throwsNoRedoableStateException() {
        VersionedWishBook versionedWishBook = prepareWishBookList(
                emptyWishBook, wishBookWithAmy, wishBookWithBob);

        assertThrows(VersionedWishBook.NoRedoableStateException.class, versionedWishBook::redo);
    }

    @Test
    public void equals() {
        VersionedWishBook versionedWishBook = prepareWishBookList(wishBookWithAmy, wishBookWithBob);

        // same values -> returns true
        VersionedWishBook copy = prepareWishBookList(wishBookWithAmy, wishBookWithBob);
        assertTrue(versionedWishBook.equals(copy));

        // same object -> returns true
        assertTrue(versionedWishBook.equals(versionedWishBook));

        // null -> returns false
        assertFalse(versionedWishBook.equals(null));

        // different types -> returns false
        assertFalse(versionedWishBook.equals(1));

        // different state list -> returns false
        VersionedWishBook differentWishBookList = prepareWishBookList(wishBookWithBob, wishBookWithCarl);
        assertFalse(versionedWishBook.equals(differentWishBookList));

        // different current pointer index -> returns false
        VersionedWishBook differentCurrentStatePointer = prepareWishBookList(
                wishBookWithAmy, wishBookWithBob);
        shiftCurrentStatePointerLeftwards(versionedWishBook, 1);
        assertFalse(versionedWishBook.equals(differentCurrentStatePointer));
    }

    /**
     * Asserts that {@code versionedWishBook} is currently pointing at {@code expectedCurrentState},
     * states before {@code versionedWishBook#currentStatePointer} is equal to {@code expectedStatesBeforePointer},
     * and states after {@code versionedWishBook#currentStatePointer} is equal to {@code expectedStatesAfterPointer}.
     */
    private void assertWishBookListStatus(VersionedWishBook versionedWishBook,
                                          List<ReadOnlyWishBook> expectedStatesBeforePointer,
                                          ReadOnlyWishBook expectedCurrentState,
                                          List<ReadOnlyWishBook> expectedStatesAfterPointer) {
        // check state currently pointing at is correct
        assertEquals(new WishBook(versionedWishBook), expectedCurrentState);

        // shift pointer to start of state list
        while (versionedWishBook.canUndo()) {
            versionedWishBook.undo();
        }

        // check states before pointer are correct
        for (ReadOnlyWishBook expectedWishBook : expectedStatesBeforePointer) {
            assertEquals(expectedWishBook, new WishBook(versionedWishBook));
            versionedWishBook.redo();
        }

        // check states after pointer are correct
        for (ReadOnlyWishBook expectedWishBook : expectedStatesAfterPointer) {
            versionedWishBook.redo();
            assertEquals(expectedWishBook, new WishBook(versionedWishBook));
        }

        // check that there are no more states after pointer
        assertFalse(versionedWishBook.canRedo());

        // revert pointer to original position
        expectedStatesAfterPointer.forEach(unused -> versionedWishBook.undo());
    }

    /**
     * Creates and returns a {@code VersionedWishBook} with the {@code wishBookStates} added into it, and the
     * {@code VersionedWishBook#currentStatePointer} at the end of list.
     */
    private VersionedWishBook prepareWishBookList(ReadOnlyWishBook... wishBookStates) {
        assertFalse(wishBookStates.length == 0);

        VersionedWishBook versionedWishBook = new VersionedWishBook(wishBookStates[0]);
        for (int i = 1; i < wishBookStates.length; i++) {
            versionedWishBook.resetData(wishBookStates[i]);
            versionedWishBook.commit();
        }

        return versionedWishBook;
    }

    /**
     * Shifts the {@code versionedWishBook#currentStatePointer} by {@code count} to the left of its list.
     */
    private void shiftCurrentStatePointerLeftwards(VersionedWishBook versionedWishBook, int count) {
        for (int i = 0; i < count; i++) {
            versionedWishBook.undo();
        }
    }
}
<file_sep>/src/test/java/seedu/address/storage/XmlWishBookStorageTest.java
package seedu.address.storage;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static seedu.address.testutil.TypicalWishes.ALICE;
import static seedu.address.testutil.TypicalWishes.HOON;
import static seedu.address.testutil.TypicalWishes.IDA;
import static seedu.address.testutil.TypicalWishes.getTypicalWishBook;

import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;

import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.rules.TemporaryFolder;

import seedu.address.commons.exceptions.DataConversionException;
import seedu.address.model.ReadOnlyWishBook;
import seedu.address.model.WishBook;

public class XmlWishBookStorageTest {
    private static final Path TEST_DATA_FOLDER = Paths.get("src", "test", "data", "XmlWishBookStorageTest");

    @Rule
    public ExpectedException thrown = ExpectedException.none();

    @Rule
    public TemporaryFolder testFolder = new TemporaryFolder();

    @Test
    public void readWishBook_nullFilePath_throwsNullPointerException() throws Exception {
        thrown.expect(NullPointerException.class);
        readWishBook(null);
    }

    private java.util.Optional<ReadOnlyWishBook> readWishBook(String filePath) throws Exception {
        return new XmlWishBookStorage(Paths.get(filePath)).readWishBook(addToTestDataPathIfNotNull(filePath));
    }

    private Path addToTestDataPathIfNotNull(String prefsFileInTestDataFolder) {
        return prefsFileInTestDataFolder != null
                ? TEST_DATA_FOLDER.resolve(prefsFileInTestDataFolder)
                : null;
    }

    @Test
    public void read_missingFile_emptyResult() throws Exception {
        assertFalse(readWishBook("NonExistentFile.xml").isPresent());
    }

    @Test
    public void read_notXmlFormat_exceptionThrown() throws Exception {

        thrown.expect(DataConversionException.class);
        readWishBook("NotXmlFormatWishBook.xml");

        /* IMPORTANT: Any code below an exception-throwing line (like the one above) will be ignored.
         * That means you should not have more than one exception test in one method
         */
    }

    @Test
    public void readWishBook_invalidWishWishBook_throwDataConversionException() throws Exception {
        thrown.expect(DataConversionException.class);
        readWishBook("invalidWishWishBook.xml");
    }

    @Test
    public void readWishBook_invalidAndValidWishWishBook_throwDataConversionException() throws Exception {
        thrown.expect(DataConversionException.class);
        readWishBook("invalidAndValidWishWishBook.xml");
    }

    @Test
    public void readAndSaveWishBook_allInOrder_success() throws Exception {
        Path filePath = testFolder.getRoot().toPath().resolve("TempWishBook.xml");
        WishBook original = getTypicalWishBook();
        XmlWishBookStorage xmlWishBookStorage = new XmlWishBookStorage(filePath);

        //Save in new file and read back
        xmlWishBookStorage.saveWishBook(original, filePath);
        ReadOnlyWishBook readBack = xmlWishBookStorage.readWishBook(filePath).get();
        assertEquals(original, new WishBook(readBack));

        //Modify data, overwrite exiting file, and read back
        original.addWish(HOON);
        original.removeWish(ALICE);
        xmlWishBookStorage.saveWishBook(original, filePath);
        readBack = xmlWishBookStorage.readWishBook(filePath).get();
        assertEquals(original, new WishBook(readBack));

        //Save and read without specifying file path
        original.addWish(IDA);
        xmlWishBookStorage.saveWishBook(original); //file path not specified
        readBack = xmlWishBookStorage.readWishBook().get(); //file path not specified
        assertEquals(original, new WishBook(readBack));

    }

    @Test
    public void saveWishBook_nullAddressBook_throwsNullPointerException() {
        thrown.expect(NullPointerException.class);
        saveWishBook(null, "SomeFile.xml");
    }

    /**
     * Saves {@code wishbook} at the specified {@code filePath}.
     */
    private void saveWishBook(ReadOnlyWishBook wishBook, String filePath) {
        try {
            new XmlWishBookStorage(Paths.get(filePath))
                    .saveWishBook(wishBook, addToTestDataPathIfNotNull(filePath));
        } catch (IOException ioe) {
            throw new AssertionError("There should not be an error writing to the file.", ioe);
        }
    }

    @Test
    public void saveWishBook_nullFilePath_throwsNullPointerException() {
        thrown.expect(NullPointerException.class);
        saveWishBook(new WishBook(), null);
    }

}
<file_sep>/src/main/java/seedu/address/storage/XmlAdaptedWish.java
package seedu.address.storage;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;

import javax.xml.bind.annotation.XmlElement;

import seedu.address.commons.exceptions.IllegalValueException;
import seedu.address.model.tag.Tag;
import seedu.address.model.wish.Date;
import seedu.address.model.wish.Name;
import seedu.address.model.wish.Price;
import seedu.address.model.wish.Remark;
import seedu.address.model.wish.SavedAmount;
import seedu.address.model.wish.Url;
import seedu.address.model.wish.Wish;

/**
 * JAXB-friendly version of the Wish.
 */
public class XmlAdaptedWish {

    public static final String MISSING_FIELD_MESSAGE_FORMAT = "Wish's %s field is missing!";

    @XmlElement(required = true)
    private String name;
    @XmlElement(required = true)
    private String price;
    @XmlElement(required = true)
    private String savedAmount;
    @XmlElement(required = true)
    private String date;
    @XmlElement(required = true)
    private String url;
    @XmlElement(required = true)
    private String remark;
    @XmlElement(required = true)
    private String id;

    @XmlElement
    private List<XmlAdaptedTag> tagged = new ArrayList<>();

    /**
     * Constructs an XmlAdaptedWish.
     * This is the no-arg constructor that is required by JAXB.
     */
    public XmlAdaptedWish() {}

    /**
     * Constructs an {@code XmlAdaptedWish} with the given wish details.
     */
    public XmlAdaptedWish(String name, String price, String date, String url, List<XmlAdaptedTag> tagged, String id) {
        this.name = name;
        this.price = price;
        this.date = date;
        this.url = url;
        this.savedAmount = "0.00";
        this.remark = "";
        if (tagged != null) {
            this.tagged = new ArrayList<>(tagged);
        }
        this.id = id;
    }

    /**
     * Converts a given Wish into this class for JAXB use.
     *
     * @param source future changes to this will not affect the created XmlAdaptedWish
     */
    public XmlAdaptedWish(Wish source) {
        name = source.getName().fullName;
        price = source.getPrice().toString();
        date = source.getDate().date;
        url = source.getUrl().value;
        savedAmount = source.getSavedAmount().toString();
        remark = source.getRemark().value;
        tagged = source.getTags().stream()
                .map(XmlAdaptedTag::new)
                .collect(Collectors.toList());
        id = source.getId().toString();
    }

    /**
     * Converts this jaxb-friendly adapted wish object into the model's Wish object.
     *
     * @throws IllegalValueException if there were any data constraints violated in the adapted wish
     */
    public Wish toModelType() throws IllegalValueException {
        final List<Tag> wishTags = new ArrayList<>();
        for (XmlAdaptedTag tag : tagged) {
            wishTags.add(tag.toModelType());
        }

        if (id == null) {
            throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, UUID.class.getSimpleName()));
        }
        final UUID modelId = UUID.fromString(id);

        if (name == null) {
            throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Name.class.getSimpleName()));
        }
        if (!Name.isValidName(name)) {
            throw new IllegalValueException(Name.MESSAGE_NAME_CONSTRAINTS);
        }
        final Name modelName = new Name(name);

        if (price == null) {
            throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Price.class.getSimpleName()));
        }
        if (!Price.isValidPrice(price)) {
            throw new IllegalValueException(Price.MESSAGE_PRICE_CONSTRAINTS);
        }
        final Price modelPrice = new Price(price);

        if (date == null) {
            throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Date.class.getSimpleName()));
        }
        if (!Date.isValidDate(date)) {
            throw new IllegalValueException(Date.MESSAGE_DATE_CONSTRAINTS);
        }
        final Date modelDate = new Date(date);

        if (url == null) {
            throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Url.class.getSimpleName()));
        }
        if (!Url.isValidUrl(url)) {
            throw new IllegalValueException(Url.MESSAGE_URL_CONSTRAINTS);
        }
        final Url modelUrl = new Url(url);

        if (this.remark == null) {
            throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Remark.class.getSimpleName()));
        }
        final Remark modelRemark = new Remark(this.remark);

        if (this.savedAmount == null) {
            throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT,
                    SavedAmount.class.getSimpleName()));
        }
        final SavedAmount modelSavedAmount = new SavedAmount(this.savedAmount);
        final Set<Tag> modelTags = new HashSet<>(wishTags);
        return new Wish(modelName, modelPrice, modelDate, modelUrl, modelSavedAmount, modelRemark, modelTags, modelId);
    }

    @Override
    public boolean equals(Object other) {
        if (other == this) {
            return true;
        }

        if (!(other instanceof XmlAdaptedWish)) {
            return false;
        }

        XmlAdaptedWish otherWish = (XmlAdaptedWish) other;
        return Objects.equals(name, otherWish.name)
                && Objects.equals(price, otherWish.price)
                && Objects.equals(date, otherWish.date)
                && Objects.equals(url, otherWish.url)
                && Objects.equals(remark, otherWish.remark)
                && tagged.equals(otherWish.tagged)
                && id.equals(otherWish.id);
    }
}
<file_sep>/src/test/java/seedu/address/storage/XmlAdaptedWishWrapperTest.java
package seedu.address.storage;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static seedu.address.testutil.TypicalWishes.getTypicalWishes;

import java.util.LinkedList;
import java.util.stream.Collectors;

import org.junit.Before;
import org.junit.Test;

/**
 * This class performs a unit test on {@code XmlAdaptedWishWrapper} class.
 */
public class XmlAdaptedWishWrapperTest {

    private XmlAdaptedWishWrapper emptyWrapper;
    private XmlAdaptedWishWrapper populatedWrapper;

    private LinkedList<XmlAdaptedWish> xmlAdaptedWishes;

    @Before
    public void init() {
        this.xmlAdaptedWishes = getTypicalWishes().stream()
                .map(wish -> new XmlAdaptedWish(wish))
                .collect(Collectors.toCollection(LinkedList::new));
        this.populatedWrapper = new XmlAdaptedWishWrapper(xmlAdaptedWishes);
        this.emptyWrapper = new XmlAdaptedWishWrapper(new LinkedList<>());
    }

    @Test(expected = NullPointerException.class)
    public void createWrapperShouldThrowException() {
        new XmlAdaptedWishWrapper(null);
    }

    @Test
    public void shouldRetrieveXmlAdaptedWishes() {
        assertEquals(emptyWrapper.getXmlAdaptedWishes(), new LinkedList<>());
        assertEquals(populatedWrapper.getXmlAdaptedWishes(), xmlAdaptedWishes);
    }

    @Test
    public void shouldBeEqual() {
        assertTrue(emptyWrapper.equals(new XmlAdaptedWishWrapper()));
        assertTrue(populatedWrapper.equals(new XmlAdaptedWishWrapper(xmlAdaptedWishes)));
    }
}
<file_sep>/src/test/java/seedu/address/logic/parser/MoveCommandParserTest.java
package seedu.address.logic.parser;

import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
import static seedu.address.logic.commands.CommandTestUtil.VALID_SAVED_AMOUNT_AMY;
import static seedu.address.logic.commands.MoveCommand.MESSAGE_USAGE;
import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure;
import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess;
import static seedu.address.logic.parser.MoveCommandParser.MESSAGE_INVALID_SAME_INDEX;
import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_WISH;
import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND_WISH;

import org.junit.Test;

import seedu.address.commons.core.amount.Amount;
import seedu.address.commons.core.index.Index;
import seedu.address.logic.commands.MoveCommand;

public class MoveCommandParserTest {
    private MoveCommandParser moveCommandParser = new MoveCommandParser();
    private Index fromIndex = INDEX_FIRST_WISH;
    private Index toIndex = INDEX_SECOND_WISH;
    private int unusedFundsIndex = MoveCommandParser.UNUSED_FUNDS_INDEX;
    private Amount amountToMove = new Amount(VALID_SAVED_AMOUNT_AMY);

    @Test
    public void parse_indexSpecified_success() {
        final String userInputWishToWish = fromIndex.getOneBased() + " " + toIndex.getOneBased()
                + " " + amountToMove.toString();
        final MoveCommand expectedCommandWishToWish = new MoveCommand(fromIndex, toIndex, amountToMove,
                MoveCommand.MoveType.WISH_TO_WISH);
        assertParseSuccess(moveCommandParser, userInputWishToWish, expectedCommandWishToWish);

        final String userInputUnusedFundsToWish = unusedFundsIndex + " " + toIndex.getOneBased()
                + " " + amountToMove.toString();
        final MoveCommand expectedCommandUnusedFundsToWish = new MoveCommand(null, toIndex, amountToMove,
                MoveCommand.MoveType.WISH_FROM_UNUSED_FUNDS);
        assertParseSuccess(moveCommandParser, userInputUnusedFundsToWish, expectedCommandUnusedFundsToWish);

        final String userInputWishToUnusedFunds = fromIndex.getOneBased() + " " + unusedFundsIndex + " "
                + amountToMove.toString();
        final MoveCommand expectedCommandWishToUnusedFunds = new MoveCommand(fromIndex, null, amountToMove,
                MoveCommand.MoveType.WISH_TO_UNUSED_FUNDS);
        assertParseSuccess(moveCommandParser, userInputWishToUnusedFunds, expectedCommandWishToUnusedFunds);
    }

    @Test
    public void parse_missingCompulsoryField_failure() {
        final String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, MESSAGE_USAGE);

        final String userInputMissingIndex = fromIndex.getZeroBased() + " " + amountToMove.toString();
        assertParseFailure(moveCommandParser, userInputMissingIndex, expectedMessage);

        final String userInputMissingAmount = fromIndex.getZeroBased() + " " + toIndex.getZeroBased() + " ";
        assertParseFailure(moveCommandParser, userInputMissingAmount, expectedMessage);
    }

    @Test
    public void parse_sameIndex_failure() {
        //FROM and TO Index cannot be the same
        final String expectedMessage = MESSAGE_INVALID_SAME_INDEX;

        final String userInputSameIndex = fromIndex.getZeroBased() + " " + fromIndex.getZeroBased() + " "
                + amountToMove.toString();
        assertParseFailure(moveCommandParser, userInputSameIndex, expectedMessage);
    }
}
<file_sep>/src/main/java/seedu/address/model/WishTransaction.java
package seedu.address.model;

import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.UUID;
import java.util.logging.Logger;
import java.util.stream.Collectors;

import seedu.address.commons.core.LogsCenter;
import seedu.address.model.tag.Tag;
import seedu.address.model.wish.Wish;

/**
 * Each {@code WishTransaction} represents a state of the WishBook.
 * A state contains logs of undeleted wish histories.
 */
public class WishTransaction implements ActionCommandListener<WishTransaction> {

    /**
     * Stores a log of wish histories for this current state.
     */
    protected HashMap<UUID, LinkedList<Wish>> wishMap;

    /**
     * Logger associated with this class.
     */
    private final Logger logger;

    public WishTransaction() {
        this.logger = getLogger();
        this.wishMap = new HashMap<>();
    }

    /**
     * Creates a WishTransaction using a saved copy of {@code WishTransaction}.
     *
     * @param savedCopy saved copy of {@code WishTransaction} stored in file.
     */
    public WishTransaction(WishTransaction savedCopy) {
        this.logger = savedCopy.logger;
        resetData(savedCopy);
    }

    /**
     * Creates a WishTransaction using a {@code ReadOnlyWishBook}.
     *
     * @param readOnlyWishBook referenced data-containing {@code ReadOnlyWishBook}.
     */
    public WishTransaction(ReadOnlyWishBook readOnlyWishBook) {
        this();
        extractData(readOnlyWishBook);
    }

    /**
     * Constructor to be called when converting XmlWishTransactions object to a WishTransaction object.
     */
    public WishTransaction(HashMap<UUID, LinkedList<Wish>> wishMap) {
        this.logger = getLogger();
        this.wishMap = wishMap;
    }

    /**
     * Returns a deep copy of the the given {@code wishTransaction} object.
     */
    public WishTransaction getCopy(WishTransaction wishTransaction) {
        WishTransaction copy = new WishTransaction();
        wishTransaction.getWishMap().entrySet().forEach(entry -> {
            copy.wishMap.put(entry.getKey(), getCopy(entry.getValue()));
        });
        return copy;
    }

    private LinkedList<Wish> getCopy(LinkedList<Wish> wishList) {
        LinkedList<Wish> copy = new LinkedList<>();
        for (Wish wish : wishList) {
            copy.add(new Wish(wish));
        }
        return copy;
    }

    private Logger getLogger() {
        return LogsCenter.getLogger(WishTransaction.class);
    }

    /**
     * Extracts data from a {@code ReadOnlyWishBook} object and seeds that data into {@code wishMap}.
     * @param wishBook object containing data to seed this object with.
     */
    protected void extractData(ReadOnlyWishBook wishBook) {
        logger.info("Extracting data from ReadOnlyWishBook");
        for (Wish wish : wishBook.getWishList()) {
            addWish(wish);
        }
    }

    /**
     * Adds a wish to {@code wishMap} using {@code wish} full name as key.
     * @param wish
     */
    @Override
    public void addWish(Wish wish) {
        UUID key = getKey(wish);
        LinkedList<Wish> wishList = updateWishes(getWishList(key), wish);
        setValueOfKey(wish, wishList);
    }

    /**
     * Returns the key corresponding to this wish.
     * @param wish queried wish.
     * @return key for the corresponding wish.
     */
    private UUID getKey(Wish wish) {
        return wish.getId();
    }

    /**
     * Retrieves the wishlist stored at {@code key}.
     * @param key name of the wish.
     * @return wishlist stored at {@code key}.
     */
    private LinkedList<Wish> getWishList(UUID key) {
        return this.wishMap.getOrDefault(key, new LinkedList<>());
    }

    /**
     * Sets the value of an existing wish to {@code wishes}.
     * @param existing an existing wish.
     * @param wishes value to be changed to.
     */
    private void setValueOfKey(Wish existing, LinkedList<Wish> wishes) {
        this.wishMap.put(getKey(existing), wishes);
    }

    /**
     * Appends the updated wish to the log of saving history for this wish.
     * @param existingWishes log of saving history.
     * @param editedWish wish to be updated to.
     * @return an updated log of saving history.
     */
    private LinkedList<Wish> updateWishes(LinkedList<Wish> existingWishes, Wish editedWish) {
        existingWishes.add(editedWish);
        return existingWishes;
    }

    /**
     * Replaces the given wish {@code target} in the list with {@code editedWish}.
     * {@code target} must exist in the wish book.
     * The wish identity of {@code editedWish} must not be the same as another existing wish in the wish book.
     */
    @Override
    public void updateWish(Wish target, Wish editedWish) {
        // get a reference to the stored wishes
        LinkedList<Wish> wishes = getWishList(getKey(target));
        // update the stored wishes
        setValueOfKey(target, updateWishes(wishes, editedWish));
    }

    /**
     * @see WishTransaction#removeWish(UUID).
     */
    @Override
    public void removeWish(Wish wish) {
        removeWish(getKey(wish));
    }

    /**
     * Removes the wish specified by {@code key}.
     *
     * @param key name of wish to remove.
     * @throws NoSuchElementException if key does not exist.
     */
    private void removeWish(UUID key) throws NoSuchElementException {
        wishMap.remove(key);
    }

    /**
     * Changes recorded log of wish histories for the current state to the {@code newData}.
     *
     * @param newData Revisioned log of wish histories.
     */
    @Override
    public void resetData(WishTransaction newData) {
        setWishMap(newData.wishMap);
    }

    /**
     * @see WishTransaction#resetData(WishTransaction)
     */
    @Override
    public void resetData() {
        wishMap.clear();
    }

    /**
     * Responds to request to remove tag from all wishes.
     * @param tag tag to be removed.
     */
    @Override
    public void removeTagFromAll(Tag tag) {
        HashMap<UUID, LinkedList<Wish>> copiedWishMap = new HashMap<>();
        for (Map.Entry<UUID, LinkedList<Wish>> entries : wishMap.entrySet()) {
            LinkedList<Wish> wishes = entries.getValue();
            // associated wish has a recorded history
            if (wishes != null) {
                copiedWishMap = getModifiedMap(tag, wishes, copiedWishMap);
            }
        }
        setWishMap(copiedWishMap);
    }

    /**
     * Removes the given {@code tag} from the given list of {@code wish}es if the tag is present.
     * @param tag tag to be removed.
     * @param wishes list of wishes to be searched for tag.
     */
    private HashMap<UUID, LinkedList<Wish>> getModifiedMap(Tag tag, LinkedList<Wish> wishes,
                                                           HashMap<UUID, LinkedList<Wish>> map) {
        Wish mostRecent = getMostRecentWish(wishes);
        if (hasTag(tag, mostRecent)) {
            return getModifiedMap(tag, mostRecent, map);
        } else {
            map.put(getKey(mostRecent), wishes);
        }
        return map;
    }

    /**
     * Removes the given tag from the given wish.
     * @param tag tag to be removed.
     * @param mostRecent wish to remove the tag from.
     */
    private HashMap<UUID, LinkedList<Wish>> getModifiedMap(Tag tag, Wish mostRecent, HashMap<UUID,
            LinkedList<Wish>> map) {
        Set<Tag> updatedTags = getUpdatedTags(mostRecent, tag);
        Wish updatedWish = getUpdatedWish(updatedTags, mostRecent);
        LinkedList<Wish> wishList = getWishList(getKey(mostRecent));
        map.put(getKey(mostRecent), updateWishes(wishList, updatedWish));
        return map;
    }

    /**
     * Returns the most recent wish recorded.
     * @param wishes list to source for the most recent wish.
     * @return the most recent wish in {@code wishes}.
     */
    private Wish getMostRecentWish(LinkedList<Wish> wishes) {
        return wishes.get(wishes.size() - 1);
    }

    /**
     * Checks if the given wish contains the given tag.
     * @param tag to be checked in the wish.
     * @param mostRecent wish to be checked for tag.
     * @return true if the tag is found in the wish, false otherwise.
     */
    private boolean hasTag(Tag tag, Wish mostRecent) {
        return mostRecent.getTags().contains(tag);
    }

    /**
     * Retrieves the updated tag set belonging to a given wish.
     * @param mostRecent the given wish.
     * @param tag the tag to be removed from the old tag set.
     * @return the new tag set without the given {@code tag}.
     */
    private Set<Tag> getUpdatedTags(Wish mostRecent, Tag tag) {
        Set<Tag> oldTags = mostRecent.getTags();
        return oldTags.stream().filter(t->(!t.equals(tag))).collect(Collectors.toSet());
    }

    /**
     * Returns a new wish with the tags changed to {@code updatedTags}.
     * @param updatedTags field to update.
     * @param target wish to be updated.
     * @return a new updated wish.
     */
    private Wish getUpdatedWish(Set<Tag> updatedTags, Wish target) {
        return new Wish(target.getName(), target.getPrice(), target.getDate(),
                target.getUrl(), target.getSavedAmount(), target.getRemark(), updatedTags, target.getId());
    }

    /**
     * Sets the current state's wish histories to {@code wishMap}.
     * @param wishMap updated wish history log.
     */
    public void setWishMap(HashMap<UUID, LinkedList<Wish>> wishMap) {
        this.wishMap = wishMap;
    }

    public HashMap<UUID, LinkedList<Wish>> getWishMap() {
        return wishMap;
    }

    /**
     * Checks if this object has any values stored in it.
     */
    public boolean isEmpty() {
        return wishMap.isEmpty();
    }

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof WishTransaction) {
            for (Map.Entry<UUID, LinkedList<Wish>> entries : ((WishTransaction) obj).wishMap.entrySet()) {
                if (!wishMap.containsKey(entries.getKey()) || !wishMap.containsValue(entries.getValue())) {
                    return false;
                }
            }
            return true;
        }
        return false;
    }

    @Override
    public String toString() {
        wishMap.entrySet().forEach(entry -> {
            System.out.println(entry.getKey());
            entry.getValue().forEach(System.out::println);
        });
        return null;
    }
}
<file_sep>/src/main/java/seedu/address/logic/parser/ParserUtil.java
package seedu.address.logic.parser;

import static java.util.Objects.requireNonNull;

import java.time.LocalDate;
import java.time.Period;
import java.time.ZoneId;
import java.time.format.DateTimeParseException;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

import seedu.address.commons.core.Messages;
import seedu.address.commons.core.index.Index;
import seedu.address.commons.util.StringUtil;
import seedu.address.logic.parser.exceptions.ParseException;
import seedu.address.model.tag.Tag;
import seedu.address.model.wish.Date;
import seedu.address.model.wish.Name;
import seedu.address.model.wish.Price;
import seedu.address.model.wish.Remark;
import seedu.address.model.wish.Url;

/**
 * Contains utility methods used for parsing strings in the various *Parser classes.
 */
public class ParserUtil {

    public static final String MESSAGE_INVALID_INDEX = "Index is not a non-zero unsigned integer.";

    /**
     * Parses {@code oneBasedIndex} into an {@code Index} and returns it. Leading and trailing whitespaces will be
     * trimmed.
     * @throws ParseException if the specified index is invalid (not non-zero unsigned integer).
     */
    public static Index parseIndex(String oneBasedIndex) throws ParseException {
        String trimmedIndex = oneBasedIndex.trim();
        if (!StringUtil.isNonZeroUnsignedInteger(trimmedIndex)) {
            throw new ParseException(MESSAGE_INVALID_INDEX);
        }
        return Index.fromOneBased(Integer.parseInt(trimmedIndex));
    }

    /**
     * Parses a {@code String name} into a {@code Name}.
     * Leading and trailing whitespaces will be trimmed.
     *
     * @throws ParseException if the given {@code name} is invalid.
     */
    public static Name parseName(String name) throws ParseException {
        requireNonNull(name);
        String trimmedName = name.trim();
        if (!Name.isValidName(trimmedName)) {
            throw new ParseException(Name.MESSAGE_NAME_CONSTRAINTS);
        }
        return new Name(trimmedName);
    }

    /**
     * Parses a {@code String price} into a {@code Price}.
     * Leading and trailing whitespaces will be trimmed.
     *
     * @throws ParseException if the given {@code price} is invalid.
     */
    public static Price parsePrice(String price) throws ParseException {
        requireNonNull(price);
        String trimmedPrice = price.trim();
        if (!Price.isValidPrice(trimmedPrice)) {
            throw new ParseException(Price.MESSAGE_PRICE_CONSTRAINTS);
        }
        return new Price(trimmedPrice);
    }

    /**
     * Parses a {@code String url} into an {@code Url}.
     * Leading and trailing whitespaces will be trimmed.
     *
     * @throws ParseException if the given {@code url} is invalid.
     */
    public static Url parseUrl(String url) throws ParseException {
        requireNonNull(url);
        String trimmedUrl = url.trim();
        if (!Url.isValidUrl(trimmedUrl)) {
            throw new ParseException(Url.MESSAGE_URL_CONSTRAINTS);
        }
        return new Url(trimmedUrl);
    }

    /**
     * Parses a {@code String tag} into a {@code Tag}.
     * Leading and trailing whitespaces will be trimmed.
     *
     * @throws ParseException if the given {@code tag} is invalid.
     */
    public static Tag parseTag(String tag) throws ParseException {
        requireNonNull(tag);
        String trimmedTag = tag.trim();
        if (!Tag.isValidTagName(trimmedTag)) {
            throw new ParseException(Tag.MESSAGE_TAG_CONSTRAINTS);
        }
        return new Tag(trimmedTag);
    }

    /**
     * Parses {@code Collection<String> tags} into a {@code Set<Tag>}.
     */
    public static Set<Tag> parseTags(Collection<String> tags) throws ParseException {
        requireNonNull(tags);
        final Set<Tag> tagSet = new HashSet<>();
        for (String tagName : tags) {
            tagSet.add(parseTag(tagName));
        }
        return tagSet;
    }

    /**
     * Parses a {@code String remark} into a {@code Remark}.
     */
    public static Remark parseRemark(String remark) throws ParseException {
        requireNonNull(remark);
        String trimmedRemark = remark.trim();
        return new Remark(trimmedRemark);
    }

    /**
     * Parses a {@code String date} into a {@code Date}.
     */
    public static Date parseDate(String date) throws ParseException {
        requireNonNull(date);
        String trimmedDate = date.trim();

        if (!Date.isValidDate(trimmedDate)) {
            throw new ParseException(Date.MESSAGE_DATE_CONSTRAINTS);
        }
        try {
            if (!Date.isFutureDate(trimmedDate)) {
                throw new ParseException(Date.MESSAGE_DATE_OUTDATED);
            }
        } catch (java.text.ParseException pe) {
            throw new ParseException(Date.MESSAGE_DATE_CONSTRAINTS);
        }
        return new Date(trimmedDate);
    }

    /**
     * Parses a {@code String period} into a {@code Period}.
     */
    public static Period parsePeriod(String period) throws ParseException {
        requireNonNull(period);
        String trimmedPeriod = period.trim();
        try {
            Period.parse("P" + trimmedPeriod);
        } catch (DateTimeParseException e) {
            throw new ParseException(String.format(Messages.MESSAGE_INVALID_TIME_FORMAT, period));
        }

        return Period.parse("P" + trimmedPeriod);
    }


    /**
     * Returns the {@code Date}, offset by an input {@code Period} from now.
     * @param periodString
     * @return
     * @throws ParseException
     */
    public static Date getPeriodOffsetFromNow(String periodString) throws ParseException {
        Period period = parsePeriod(periodString);
        java.util.Date now = new java.util.Date();
        LocalDate localNow = now.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
        LocalDate finalDate = localNow.plus(period);
        return ParserUtil.parseDate(String.format("%02d/%02d/%d", finalDate.getDayOfMonth(),
                finalDate.getMonth().getValue(),
                finalDate.getYear()));
    }
}
<file_sep>/src/main/java/seedu/address/model/wish/SavedAmount.java
package seedu.address.model.wish;

import static java.util.Objects.requireNonNull;
import static seedu.address.commons.util.AppUtil.checkArgument;

import java.text.DecimalFormat;

import seedu.address.commons.core.amount.Amount;

/**
 * Represents a saved Amount for a Wish in the wish book.
 * Guarantees: immutable; is valid as declared in {@link #isValidSavedAmount(String)}
 */
public class SavedAmount {
    public static final String MESSAGE_SAVED_AMOUNT_INVALID = "Invalid saved amount value!";
    public static final String MESSAGE_SAVED_AMOUNT_NEGATIVE = "Invalid. Saved amount causes current value to "
            + "become negative";
    public static final String MESSAGE_SAVED_AMOUNT_TOO_LARGE = "Current saved amount for wish is too large!";
    public static final String SAVED_AMOUNT_VALIDATION_REGEX = "[-+]?[0-9]+([.]{1}[0-9]{1,2})?";
    public final Double value;
    private DecimalFormat currencyFormat = new DecimalFormat("#.##");

    /**
     * Constructs a {@code SavedAmount}.
     *
     * @param savedAmount A valid savedAmount number.
     */
    public SavedAmount(String savedAmount) throws IllegalArgumentException {
        requireNonNull(savedAmount);
        checkArgument(isValidSavedAmount(savedAmount), MESSAGE_SAVED_AMOUNT_INVALID);
        value = Double.parseDouble(savedAmount);
        if (value.doubleValue() < 0) {
            throw new IllegalArgumentException(MESSAGE_SAVED_AMOUNT_NEGATIVE);
        } else if (value.doubleValue() > 1000e12) {
            throw new IllegalArgumentException(MESSAGE_SAVED_AMOUNT_TOO_LARGE);
        }
    }

    public SavedAmount(SavedAmount savedAmount) {
        this(savedAmount.value.toString());
    }

    /**
     * Constructs a {@code SavedAmount} from an increment {@code Amount}.
     *
     * @param change A valid {@code Amount} to increment this {@code SavedAmount}.
     * @return Incremented {@code SavedAmount}.
     */
    public SavedAmount incrementSavedAmount(Amount change) {
        if (this.value + change.value < 0.0) {
            throw new IllegalArgumentException(MESSAGE_SAVED_AMOUNT_NEGATIVE);
        }

        return new SavedAmount(currencyFormat.format(this.value + change.value));
    }

    /**
     * Returns a {@code Amount} from the savedAmount.
     */
    public Amount getAmount() {
        return new Amount("" + this.value);
    }

    /**
     * Returns true if a given string is a valid savedAmount number.
     */
    public static boolean isValidSavedAmount(String test) {
        return test.matches(SAVED_AMOUNT_VALIDATION_REGEX);
    }

    @Override
    public String toString() {
        return String.format("%.2f", value);
    }

    @Override
    public boolean equals(Object other) {
        return other == this // short circuit if same object
                || (other instanceof SavedAmount // instanceof handles nulls
                && value.equals(((SavedAmount) other).value)); // state check
    }

    @Override
    public int hashCode() {
        return value.hashCode();
    }
}
<file_sep>/src/test/java/seedu/address/logic/commands/EditWishDescriptorTest.java
package seedu.address.logic.commands;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static seedu.address.logic.commands.CommandTestUtil.DESC_AMY;
import static seedu.address.logic.commands.CommandTestUtil.DESC_BOB;
import static seedu.address.logic.commands.CommandTestUtil.VALID_DATE_2;
import static seedu.address.logic.commands.CommandTestUtil.VALID_NAME_BOB;
import static seedu.address.logic.commands.CommandTestUtil.VALID_PRICE_BOB;
import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_HUSBAND;
import static seedu.address.logic.commands.CommandTestUtil.VALID_URL_BOB;

import org.junit.Test;

import seedu.address.logic.commands.EditCommand.EditWishDescriptor;
import seedu.address.testutil.EditWishDescriptorBuilder;

public class EditWishDescriptorTest {

    @Test
    public void equals() {
        // same values -> returns true
        EditWishDescriptor descriptorWithSameValues = new EditWishDescriptor(DESC_AMY);
        assertTrue(DESC_AMY.equals(descriptorWithSameValues));

        // same object -> returns true
        assertTrue(DESC_AMY.equals(DESC_AMY));

        // null -> returns false
        assertFalse(DESC_AMY.equals(null));

        // different types -> returns false
        assertFalse(DESC_AMY.equals(5));

        // different values -> returns false
        assertFalse(DESC_AMY.equals(DESC_BOB));

        // different name -> returns false
        EditWishDescriptor editedAmy = new EditWishDescriptorBuilder(DESC_AMY).withName(VALID_NAME_BOB).build();
        assertFalse(DESC_AMY.equals(editedAmy));

        // different price -> returns false
        editedAmy = new EditWishDescriptorBuilder(DESC_AMY).withPrice(VALID_PRICE_BOB).build();
        assertFalse(DESC_AMY.equals(editedAmy));

        // different email -> returns false
        editedAmy = new EditWishDescriptorBuilder(DESC_AMY).withDate(VALID_DATE_2).build();
        assertFalse(DESC_AMY.equals(editedAmy));

        // different address -> returns false
        editedAmy = new EditWishDescriptorBuilder(DESC_AMY).withAddress(VALID_URL_BOB).build();
        assertFalse(DESC_AMY.equals(editedAmy));

        // different tags -> returns false
        editedAmy = new EditWishDescriptorBuilder(DESC_AMY).withTags(VALID_TAG_HUSBAND).build();
        assertFalse(DESC_AMY.equals(editedAmy));
    }
}
<file_sep>/src/main/java/seedu/address/storage/WishBookStorage.java
package seedu.address.storage;

import java.io.IOException;
import java.nio.file.Path;
import java.util.Optional;

import seedu.address.commons.exceptions.DataConversionException;
import seedu.address.model.ReadOnlyWishBook;
import seedu.address.model.WishBook;

/**
 * Represents a storage for {@link WishBook}.
 */
public interface WishBookStorage {

    /**
     * Returns the file path of the data file.
     */
    Path getWishBookFilePath();

    /**
     * Returns WishBook data as a {@link ReadOnlyWishBook}.
     *   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<ReadOnlyWishBook> readWishBook() throws DataConversionException, IOException;

    /**
     * @see #getWishBookFilePath()
     */
    Optional<ReadOnlyWishBook> readWishBook(Path filePath) throws DataConversionException, IOException;

    /**
     * Saves the given {@link ReadOnlyWishBook} to the storage.
     * @param wishBook cannot be null.
     * @throws IOException if there was any problem writing to the file.
     */
    void saveWishBook(ReadOnlyWishBook wishBook) throws IOException;

    /**
     * @see #saveWishBook(ReadOnlyWishBook)
     */
    void saveWishBook(ReadOnlyWishBook wishBook, Path filePath) throws IOException;

    /**
     * @see #saveBackup(Path)
     */
    void saveBackup() throws IOException, DataConversionException;

    /**
     * Saves backup copy of wishbook at the corresponding filepath.
     * @param path filepath of backup file.
     */
    void saveBackup(Path path) throws IOException, DataConversionException;

    /**
     * Backs up the given {@link ReadOnlyWishBook} to local storage.
     * @param wishBook wishBook cannot be null.
     * @throws IOException if there was any problem writing to the file
     */
    void backupWishBook(ReadOnlyWishBook wishBook) throws IOException;

    /**
     * @see #saveWishBook(ReadOnlyWishBook)
     */
    void backupWishBook(ReadOnlyWishBook wishBook, Path filePath) throws IOException;
}
<file_sep>/src/test/java/seedu/address/model/tag/TagTest.java
package seedu.address.model.tag;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_FRIEND;
import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_HUSBAND;

import org.junit.Test;

import seedu.address.testutil.Assert;

public class TagTest {

    @Test
    public void constructor_null_throwsNullPointerException() {
        Assert.assertThrows(NullPointerException.class, () -> new Tag((String) null));
        Assert.assertThrows(NullPointerException.class, () -> new Tag((Tag) null));
    }

    @Test
    public void constructor_invalidTagName_throwsIllegalArgumentException() {
        String invalidTagName = "";
        Assert.assertThrows(IllegalArgumentException.class, () -> new Tag(invalidTagName));
    }

    @Test
    public void isValidTagName() {
        // null tag name
        Assert.assertThrows(NullPointerException.class, () -> Tag.isValidTagName(null));
    }

    @Test
    public void equals() {
        Tag tagHusband = new Tag(VALID_TAG_HUSBAND);
        Tag tagFriend = new Tag(VALID_TAG_FRIEND);
        // null -> return false
        assertFalse(tagHusband.equals(null));

        // same object -> returns true
        assertTrue(tagHusband.equals(tagHusband));

        // different type -> returns false
        assertFalse(tagFriend.equals(99));

        // different tag -> returns false
        assertFalse(tagHusband.equals(tagFriend));
    }

}
<file_sep>/src/main/java/seedu/address/model/ActionCommandListener.java
package seedu.address.model;

import seedu.address.model.tag.Tag;
import seedu.address.model.wish.Wish;

/**
 * This interface is implemented by classes listening on action commands.
 * An action command is defined to be a command that changes the state of the WishBook.
 * Examples of action commands include clear, delete, updateWish, add, save, removeTagFromAll, etc.
 */
public interface ActionCommandListener<T> {

    /**
     * Called when a command attempts to add a wish to the wish book.
     */
    void addWish(Wish wish);

    /**
     * Called when a command attempts to replace the given wish {@code target} with {@code editedWish}.
     * {@code target} must exist in the wish book.
     *
     */
    void updateWish(Wish target, Wish editedWish);

    /**
     * Called when a command attempts to remove {@code key} from this {@code WishBook}.
     * {@code key} must exist in the wish book.
     */
    void removeWish(Wish wish);

    /**
     * Called when action commands undo, redo are executed.
     * @param newData newData to replace the model with.
     */
    void resetData(T newData);

    /**
     * Called when action command clear is executed.
     */
    void resetData();

    /**
     * Removes {@code tag} from all {@code wish}s in this {@code WishBook}.
     */
    void removeTagFromAll(Tag tag);
}
<file_sep>/src/main/java/seedu/address/model/ReadOnlyWishBook.java
package seedu.address.model;

import javafx.collections.ObservableList;
import seedu.address.model.wish.SavedAmount;
import seedu.address.model.wish.Wish;

/**
 * Unmodifiable view of a wish book
 */
public interface ReadOnlyWishBook {

    /**
     * Returns an unmodifiable view of the wish list.
     * This list will not contain any duplicate wishes.
     */
    ObservableList<Wish> getWishList();

    /**
     * Returns an unmodifiable view of the unusedFunds amount.
     */
    SavedAmount getUnusedFunds();
}
<file_sep>/src/test/java/seedu/address/logic/parser/ListCommandParserTest.java
package seedu.address.logic.parser;

import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess;

import org.junit.Test;

import seedu.address.logic.commands.ListCommand;

public class ListCommandParserTest {
    private ListCommandParser parser = new ListCommandParser();

    @Test
    public void parse_emptyArgs_returnsListAll() {
        assertParseSuccess(parser, "   ", new ListCommand(ListCommand.ListType.SHOW_ALL));
    }

    @Test
    public void parse_validArgs_returnsCorrectList() {
        ListCommand completedListCommand = new ListCommand(ListCommand.ListType.SHOW_COMPLETED);
        ListCommand uncompletedListCommand = new ListCommand(ListCommand.ListType.SHOW_UNCOMPLETED);

        assertParseSuccess(parser, "-c", completedListCommand);
        assertParseSuccess(parser, "-u", uncompletedListCommand);
    }
}
<file_sep>/src/test/java/seedu/address/logic/commands/ListCommandTest.java
package seedu.address.logic.commands;

import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess;
import static seedu.address.logic.commands.CommandTestUtil.showWishAtIndex;
import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_WISH;
import static seedu.address.testutil.TypicalWishes.getTypicalWishBook;
import static seedu.address.testutil.TypicalWishes.getTypicalWishTransaction;

import org.junit.Before;
import org.junit.Test;

import seedu.address.logic.CommandHistory;
import seedu.address.model.Model;
import seedu.address.model.ModelManager;
import seedu.address.model.UserPrefs;
import seedu.address.model.wish.WishCompletedPredicate;

/**
 * Contains integration tests (interaction with the Model) and unit tests for ListCommand.
 */
public class ListCommandTest {

    private Model model;
    private Model expectedModel;
    private CommandHistory commandHistory = new CommandHistory();

    @Before
    public void setUp() {
        model = new ModelManager(getTypicalWishBook(), getTypicalWishTransaction(), new UserPrefs());
        expectedModel = new ModelManager(model.getWishBook(), model.getWishTransaction(), new UserPrefs());
    }

    @Test
    public void execute_listIsNotFiltered_showsSameList() {
        assertCommandSuccess(new ListCommand(ListCommand.ListType.SHOW_ALL),
                model, commandHistory, ListCommand.MESSAGE_SUCCESS, expectedModel);
    }

    @Test
    public void execute_listIsFiltered_showsEverything() {
        showWishAtIndex(model, INDEX_FIRST_WISH);
        assertCommandSuccess(new ListCommand(ListCommand.ListType.SHOW_ALL),
                model, commandHistory, ListCommand.MESSAGE_SUCCESS, expectedModel);
    }

    @Test
    public void execute_showListCompleted_success() {
        expectedModel.updateFilteredWishList(new WishCompletedPredicate(true));
        assertCommandSuccess(new ListCommand(ListCommand.ListType.SHOW_COMPLETED),
                model, commandHistory, ListCommand.MESSAGE_SHOWED_COMPLETED, expectedModel);
    }

    @Test
    public void execute_showListUncompleted_success() {
        expectedModel.updateFilteredWishList(new WishCompletedPredicate(false));
        assertCommandSuccess(new ListCommand(ListCommand.ListType.SHOW_UNCOMPLETED),
                model, commandHistory, ListCommand.MESSAGE_SHOWED_UNCOMPLETED, expectedModel);
    }
}
<file_sep>/src/main/java/seedu/address/logic/parser/MoveCommandParser.java
package seedu.address.logic.parser;

import static java.util.Objects.requireNonNull;
import static seedu.address.commons.core.Messages.MESSAGE_INVALID_AMOUNT;
import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT;

import seedu.address.commons.core.amount.Amount;
import seedu.address.commons.core.index.Index;
import seedu.address.logic.commands.MoveCommand;
import seedu.address.logic.parser.exceptions.ParseException;

/**
 * Parses input arguments and creates a new MoveCommand object.
 */
public class MoveCommandParser implements Parser<MoveCommand> {

    public static final int UNUSED_FUNDS_INDEX = 0;
    public static final String MESSAGE_INVALID_SAME_INDEX = "Invalid! FROM and TO index cannot be the same.";

    /**
     * Parses the given {@code String} of arguments in the context of the MoveCommand
     * and returns a MoveCommand object for execution.
     * @throws ParseException if the user input does not conform the expected FORMAT
     * @throws IllegalArgumentException if the user enters an invalid argument
     */
    @Override
    public MoveCommand parse(String args) throws ParseException, IllegalArgumentException {
        requireNonNull(args);

        String trimmedArgs = args.trim();
        if (trimmedArgs.isEmpty()) {
            throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, MoveCommand.MESSAGE_USAGE));
        }

        String[] arguments = trimmedArgs.split(" ");

        if (arguments.length != 3) {
            throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, MoveCommand.MESSAGE_USAGE));
        }

        MoveCommand.MoveType moveCommandType;
        Index fromIndex;
        Index toIndex;

        //Cannot parse the same index for FROM and TO
        if (arguments[0].equals(arguments[1])) {
            throw new ParseException(MESSAGE_INVALID_SAME_INDEX);
        }

        Amount amount;
        try {
            amount = new Amount(arguments[2]);
        } catch (IllegalArgumentException iae) {
            throw new ParseException(String.format(MESSAGE_INVALID_AMOUNT, arguments[2]));
        }

        try {
            if (Integer.parseInt(arguments[0]) == UNUSED_FUNDS_INDEX) {
                fromIndex = null;
                toIndex = ParserUtil.parseIndex(arguments[1]);
                moveCommandType = MoveCommand.MoveType.WISH_FROM_UNUSED_FUNDS;
            } else if (Integer.parseInt(arguments[1]) == UNUSED_FUNDS_INDEX) {
                fromIndex = ParserUtil.parseIndex(arguments[0]);
                toIndex = null;
                moveCommandType = MoveCommand.MoveType.WISH_TO_UNUSED_FUNDS;
            } else {
                fromIndex = ParserUtil.parseIndex(arguments[0]);
                toIndex = ParserUtil.parseIndex(arguments[1]);
                moveCommandType = MoveCommand.MoveType.WISH_TO_WISH;
            }
        } catch (ParseException pe) {
            throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, MoveCommand.MESSAGE_USAGE));
        }

        return new MoveCommand(fromIndex, toIndex, amount, moveCommandType);
    }
}
<file_sep>/docs/team/bannified.adoc
= <NAME> (bannified) - Project Portfolio
:site-section: AboutUs
:imagesDir: ../images
:stylesDir: ../stylesheets
ifdef::env-github[]
:tip-caption: :bulb:
:note-caption: :information_source:
:warning-caption: :warning:
:experimental:
endif::[]

== PROJECT: WishBook

---

== Overview

WishBook is a desktop application used to help one allocate one's disposable income to a number of desired products.
WishBook enables users to better organize their desirables, allowing them to visualize their spendings in a pleasant way.
The user interacts with it by using a Command Line Interface (CLI), and it has a GUI created with JavaFX.

WishBook was developed by the GaveSoals team, a team of students based in the School of Computing, National University of Singapore.

image::Ui.png[width="790"]

WishBook is an application that promotes sustainable and responsible consumerism, by having users be more frugal with their income while
rewarding themselves at appropriate times, when they can afford to.

Wishbook is an application adapted from addressbook-level4, the original application developed by the se-edu team.

The source code of addressbook-level4 can be found https://github.com/nus-cs2103-AY1819S1/addressbook-level4[here].


The main features of WishBook are:

* Add a Wish (of a desired item) to the WishBook
* Add funds to a Wish to make progress in fulfilling a Wish
* Add remarks to a Wish
* Add funds to WishBook without an explicit Wish
* Keep track of the user's progress to fulfilling a Wish, so that the user can buy it.
* Chang the details of a Wish after it has been added
* Find Wishes by name or tags
* View all completed or uncompleted Wishes
* Delete an unwanted Wish
* View of the Wish's product page (if applicable)
* Undo and Redo commands
* View history of savings

== Summary of contributions

Mentioned below are the contributions I have made with regards to WishBook's development.
Major components of the application were delegated to every person in the team, and I was tasked with the Model component.

* *Major enhancement*: Add Wish Command
** What it does: allows the user to add a `Wish` by entering a product's details.
** Justification: This feature is one of the core functionalities of the application,
as a Wish is the primary entity that a User will interact with when using WishBook.
** Highlights:

*** Since this feature is core to the application,
defensive programming has to be practiced when implementing this feature,
as the Wish will likely affect features being added in the future.

*** The Add Command also effectively creates Wish entries for the Wishbook to operate on,
and is the entry point of the lifecycle of our application's usage, bugs occurring
in this command will cascade to other Wish-related commands, hence its implementation
has to be safe and well-tested.

* *Minor enhancements*:
** Add an option for the user to state the Duration that a Wish will last, when adding a Wish with Add Command.
** Change syntax of Save Command to make it more user friendly
** Make Save Command show the change in SavedAmount of a wish from before

* *Code contributed*: https://nus-cs2103-ay1819s1.github.io/cs2103-dashboard/#=undefined&search=bannified[Project Code Dashboard link]

* *Other contributions*:

** As Team Lead of GaveSoals:
*** Managed milestones `v1.1` - `v1.4` and one `v1.5` (release) on GitHub
*** Handle distribution of tasks and features for the team
*** Ensure that development is steady and milestones are met
*** Manage issue tracker
** Enhancements to existing features:
*** Add age option (for dueDate) when adding a Wish (Pull requests https://github.com/CS2103-AY1819S1-T16-1/main/pull/74[#74])
*** Application-wide refactoring to suit our desired WishBook product (Pull requests https://github.com/CS2103-AY1819S1-T16-1/main/pull/12[#12])

** Community:
*** PR reviewed (with non-trivial comments): https://github.com/CS2103-AY1819S1-T16-1/main/pull/1[#1],
https://github.com/CS2103-AY1819S1-T16-1/main/pull/7[#7],
https://github.com/CS2103-AY1819S1-T16-1/main/pull/55[#55]
*** Reported bugs for other teams https://github.com/CS2103-AY1819S1-W12-2/main/issues/103[1],
https://github.com/CS2103-AY1819S1-W12-2/main/issues/105[2],
https://github.com/CS2103-AY1819S1-W12-2/main/issues/109[3]

== 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._
|===

== Quick Start

.  Ensure you have Java version `9` or later installed in your Computer.
.  Download the latest `wishbook.jar` link:{repoURL}/releases[here].
.  Copy the file to the folder you want to use as the home folder for your WishBook app.
.  Double-click the file to start the app. The GUI should appear in a few seconds.
+
image::Ui.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 the items you have set as wishes (sorted by due date).
* **`add`**`n/uPhoneXX p/1000 a/5m` : adds an item “uPhoneXX” as a goal to be completed in 5 months.
* *`help`*: displays list of command with usage.
* *`clear`*: clears view
* *`exit`* : exits the app


.  Refer to <<Features>> for details of each command.

[[Features]]
== Features

====
*Command Format*

* Words in `UPPER_CASE` are the parameters to be supplied by the user e.g. in `add WISH`, `WISH` is a parameter which
can be used as add iPhone.
* Items in square brackets are optional e.g. in `list [FILTER]`, `FILTER` is an optional parameter, since the list
command can be used as `list` to display all wishes in WishBook.
** An exception to this is `TIME_GIVEN` and `END_DATE`, whereby only one of the two can be used in any command.
** The `add` command requires either `TIME_GIVEN` and `END_DATE`.
** The `edit` command can take either or none of them.
* 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/broke`, `t/needs` etc.
* The `/` symbol between parameters means that you can use either of the parameters types in the command e.g. in
`add WISH PRICE [TIME_GIVEN]/[START_DATE to END_DATE]`, you provide either the `TIME_GIVEN` parameter or `START_DATE`
and `END_DATE` parameters.

====

=== Add a new wish: `add`
Add a new wish to the wish list. +
Format: `add n/WISH_NAME p/PRICE [a/PERIOD_GIVEN]/[d/END_DATE] [u/URL] [t/TAG]...`

[TIP]
* `[END_DATE]`: Specified in _dd/mm/yyyy_ format.
* `[TIME_GIVEN]`: Specified in terms of years, months, weeks, and days, suffixes (coming after the value)
marking such time periods are _‘y’_, _‘m’_, _‘w’_, and _‘d’_ respectively.

The order of `[TIME_GIVEN]` must be from the biggest unit of time to the smallest unit of time, meaning that the
suffix _`y`_ cannot come after any of the other three suffixes, and _'w'_ cannot come after _'d'_, but can come
after _'y'_ and _'m'_.

[NOTE]
====
If you enter an invalid date, a warning message will be displayed to prompt the user to reenter a valid date.
Until all fields provided are valid, the wish will not be added to `WishBook`.
====

[NOTE]
====
The expiry date you enter must be after current date.
====

Examples:

* `add n/XBoxTwo p/999 a/1y`
* `add n/kfcBook 13inch p/2300 a/6m3w r/For dad t/family t/computing`
* `add n/prinkles p/1.95 d/24/04/2020`
* `add n/prinkles p/1.95 d/24/04/2020 u/www.amazon.com/prinkles t/high`

=== Edit a wish : `edit`

Edits an existing wish in the wish list. +
Format: `edit INDEX [n/WISH_NAME] [p/PRICE] [a/TIME_GIVEN]/[d/END_DATE] [u/URL] [t/TAG]`

****
* Edits the wish at the specified `INDEX`. `INDEX` refers to the index number shown in the displayed list of goals. `INDEX` must be a positive integer 1, 2, 3, …
* `INDEX` is labelled at the side of each wish.
* 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 wish will be removed i.e. adding of tags is not cumulative.
* You can remove all tags by typing `t/` without specifying any tags after it.
****

Examples:

* `edit 1 n/Macbook Pro t/Broke wishes` +
Edits the name of the wish and the tag of the 1st wish to be Macbook Pro and Broke wishes respectively

* `edit 2 p/22 a/22w` +
Edits the price and time given to accomplish the 2nd wish to 22 (in the chosen currency) and 22 weeks respectively.

== 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._
|===

=== Add Wish feature

==== Current Implementation

The Add Wish feature is executed through an `AddCommand` by the user, which after parsing,
is facilitated mainly by the `ModelManager` which implements `Model`.
It also affects `versionedWishBook` and `versionedWishTransaction` by adding the
resultant wish to both of their respective data structures.
After adding a `Wish`, the `filteredSortedWishes` is also updated to reflect the
latest version of WishBook.
The UI is also prompted to refresh through a `WishBookChangedEvent`.

`AddCommandParser` parses the user's input for parameters using prefixes,
and checks them against their respective regular expressions (regex), specified
in their respective classes.

====

The following prefix/parameter pairs are **compulsory**, where a user's input will be
rejected if they are not provided:

* `n/`: Name
* `p/`: Price
* One of the following Date parameters:
** `d/`: Exact expiry date
** `a/`: duration (or lifetime) from time when command is entered

The following prefix/parameter pairs are **optional**, where a user's input will
be successful even if they are not provided:

* `t/`: tags (more than one allowed)
* `u/`: product's URL (product page)

====

[NOTE]
====
**Regarding Duration (`a/`) vs Date (`d/`)**

* If `d/` is used, a valid Date should be used.
** Date comes in the format of `dd/mm/yyyy`,
`dd` being days, `mm` being months, `yyyy` being the year, and the
** Specified date should also be a valid date in the future.
* If `a/` is used, a valid Duration should be used.
** length instead of `dd/mm/yyyy` format, the format should be `<years>y<months>m<days>d`.
* In any command, only `Duration` or `Date` can be used. Never both.
* If an invalid string for date or duration is provided, a warning will be
displayed to prompt the user to enter a valid date or duration.

====

Given below is an example usage scenario and how an AddCommand is carried out.

Step 1. The user types in a valid `AddCommand`, `add n/1 TB Toshiba SSD p/158 a/200d`, and the current date is 2nd October 2017 (2/10/2017).

The `AddCommandParser` will employ `ParserUtil` to parse the attributes specified after each prefix. The parsing of the
`Duration` attribute which follows `a/` in the command will be discussed below.

Since `Duration` prefix is used, the computation of a wish's expiry date is handled
internally in the `ParserUtil` class, which `ParserUtil#parseDate()` parses and converts
the input string into a `Period` object (if input is valid),
and adds the resultant `Period` to the current `Date` to get the desired
`Date` of the `Wish`.

The resultant `Wish` will have the following properties:

* id: `a randomly-generated UUID`
* Name: _1TB Toshiba SSD_
* SavedAmount: 0.00
* Price: 158.00
* Date: 20/4/2018 (20th April 2018)
* URL: `empty string`
* Remark: `empty string`
* Tags: `none`
* Fulfilled: `false`
* Expired: `false`

The resultant wish is pass into `VersionedWishBook#addWish` and `VersionedWishTransaction#addWish`,
which tracks the history of the `WishBook` and `Wish` respectively. The list of wishes shown on the UI is also updated to show all wishes again,
as `filteredSortedWishes` is updated to have all wishes in `WishBook` and a `WishBookChangedEvent` is fired.

The following sequence diagram shows how an `AddCommand` is processed in WB:

image::AddWishSequenceDiagram.png[width="800"]

Step 2. Some time later, the user decides that she wants the exact same wish,
but duplicated, and enters the exact same command, but with an exact `Date`
instead of `Duration`, so the command entered is
`add n/1 TB Toshiba SSD p/158 d/20/4/2018`.

Since `Date` prefix is used, the `ParserUtil` parses the string into a `Date` object,
and the resultant object is used directly for the resultant `Wish`.

Similar to in Step 1, the command will be parsed successfully and a second `Wish`
will be added, albeit with a different (hidden) id generated.

The resultant `Wish` will have the following properties:

* id: `another randomly-generated UUID`
* Name: _1TB Toshiba SSD_
* SavedAmount: 0.00
* Price: 158.00
* Date: 20/4/2018 (20th April 2018)
* URL: `empty string`
* Remark: `empty string`
* Tags: `none`
* Fulfilled: `false`
* Expired: `false`

==== Design Considerations
* **Alternative 1 (current choice)**: Different prefixes for `Duration` and `Date`.
** Pros: More focused user experience.
User get more specific feedback depending on their preferred way of inputting date if a wrong input was made.
If user uses `a/` and enters an incorrect `Duration`, the user will not receive an error message about the correct format
for an exact `Date`, and will only be notified of the correct format of a `Duration`.
** Pros: Easier to implement and handle isolate errors related to respective input parameters.
** Cons: More prefixes for user to remember.

* **Alternative 2**: Have `Duration` and `Date` use the same prefix.
** Pros: More natural usage of one prefix to determine `Wish` 's desired expiry date.
** Cons: Conflating implementation of `Duration` and `Date`, hence harder to debug.
** Cons: Tricky to implement, as we are parsing one input for two different desired formats.
<file_sep>/src/test/java/seedu/address/testutil/WishBuilder.java
package seedu.address.testutil;

import java.util.HashSet;
import java.util.Set;
import java.util.UUID;

import seedu.address.commons.core.amount.Amount;
import seedu.address.model.tag.Tag;
import seedu.address.model.util.SampleDataUtil;
import seedu.address.model.wish.Date;
import seedu.address.model.wish.Name;
import seedu.address.model.wish.Price;
import seedu.address.model.wish.Remark;
import seedu.address.model.wish.SavedAmount;
import seedu.address.model.wish.Url;
import seedu.address.model.wish.Wish;

/**
 * A utility class to help with building Wish objects.
 */
public class WishBuilder {

    public static final String DEFAULT_NAME = "<NAME>";
    public static final String DEFAULT_PRICE = "85.53";
    public static final String DEFAULT_SAVED_AMOUNT = "0.00";
    public static final String DEFAULT_DATE = "29/10/2021";
    public static final String DEFAULT_URL = "https://www.lazada.sg/products/"
            + "ps4-092-hori-real-arcade-pron-hayabusaps4ps3pc-i223784444-s340908955.html";
    public static final String DEFAULT_REMARK = "";
    public static final String DEFAULT_ID = "e2762cbc-ea52-4f66-aa73-b9b87cbcf004";

    private Name name;
    private Price price;
    private SavedAmount savedAmount;
    private Date date;
    private Url url;
    private Remark remark;
    private Set<Tag> tags;
    private UUID id;

    public WishBuilder() {
        name = new Name(DEFAULT_NAME);
        price = new Price(DEFAULT_PRICE);
        savedAmount = new SavedAmount(DEFAULT_SAVED_AMOUNT);
        date = new Date(DEFAULT_DATE);
        url = new Url(DEFAULT_URL);
        remark = new Remark(DEFAULT_REMARK);
        tags = new HashSet<>();
        id = UUID.fromString(DEFAULT_ID);
    }

    /**
     * Initializes the WishBuilder with the data of {@code wishToCopy}.
     */
    public WishBuilder(Wish wishToCopy) {
        name = wishToCopy.getName();
        price = wishToCopy.getPrice();
        savedAmount = wishToCopy.getSavedAmount();
        date = wishToCopy.getDate();
        url = wishToCopy.getUrl();
        remark = wishToCopy.getRemark();
        tags = new HashSet<>(wishToCopy.getTags());
        id = wishToCopy.getId();
    }

    /**
     * Sets the {@code Name} of the {@code Wish} that we are building.
     */
    public WishBuilder withName(String name) {
        this.name = new Name(name);
        return this;
    }

    /**
     * Parses the {@code tags} into a {@code Set<Tag>} and set it to the {@code Wish} that we are building.
     */
    public WishBuilder withTags(String ... tags) {
        this.tags = SampleDataUtil.getTagSet(tags);
        return this;
    }

    /**
     * Sets the {@code Url} of the {@code Wish} that we are building.
     */
    public WishBuilder withUrl(String url) {
        this.url = new Url(url);
        return this;
    }

    /**
     * Sets the {@code Price} of the {@code Wish} that we are building.
     */
    public WishBuilder withPrice(String price) {
        this.price = new Price(price);
        return this;
    }

    /**
     * Increments the default {@code SavedAmount} of the {@code Wish} to be built
     * with the the given {@code SavedAmount}.
     */
    public WishBuilder withSavedAmountIncrement(String savedAmount) {
        this.savedAmount = this.savedAmount.incrementSavedAmount(new Amount(savedAmount));
        return this;
    }

    /**
     * Sets the {@code Date} of the {@code Wish} that we are building.
     */
    public WishBuilder withDate(String date) {
        this.date = new Date(date);
        return this;
    }

    /**
     * Sets the {@code Remark} of the {@code Wish} that we are building.
     */
    public WishBuilder withRemark(String remark) {
        this.remark = new Remark(remark);
        return this;
    }

    /**
     * Sets the {@code UUID} of the {@code Wish} that we are building.
     */
    public WishBuilder withId(String id) {
        this.id = UUID.fromString(id);
        return this;
    }

    public Wish build() {
        return new Wish(name, price, date, url, savedAmount, remark, tags, id);
    }
}
<file_sep>/src/main/java/seedu/address/model/versionedmodels/VersionedModel.java
package seedu.address.model.versionedmodels;

/**
 * This interface is implemented by classes which keep track of each state of the wishbook.
 */
public interface VersionedModel {

    /**
     * Saves a copy of the current state.
     */
    void commit();

    /**
     * Restores the model to the previous state.
     */
    void undo();

    /**
     * Restores the model to its previously undone state.
     */
    void redo();

    /**
     * Thrown when trying to {@code undo()} but can't.
     */
    class NoUndoableStateException extends RuntimeException {
        NoUndoableStateException() {
            super("Current state pointer at start of wishState list, unable to undo.");
        }
    }

    /**
     * Thrown when trying to {@code redo()} but can't.
     */
    class NoRedoableStateException extends RuntimeException {
        NoRedoableStateException() {
            super("Current state pointer at end of wishState list, unable to redo.");
        }
    }
}
<file_sep>/src/main/java/seedu/address/storage/XmlAdaptedSavedAmount.java
package seedu.address.storage;

import javax.xml.bind.annotation.XmlValue;

import seedu.address.commons.exceptions.IllegalValueException;
import seedu.address.model.wish.SavedAmount;

/**
 * JAXB-friendly adapted version of the SavedAmount.
 */
public class XmlAdaptedSavedAmount {

    @XmlValue
    private String savedAmount;

    /**
     * Constructs an XmlAdaptedSavedAmount.
     * This is the no-arg constructor that is required by JAXB.
     */
    public XmlAdaptedSavedAmount() {}

    /**
     * Constructs a {@code XmlAdaptedSavedAmount} with the given {@code amount}.
     */
    public XmlAdaptedSavedAmount(String amount) {
        this.savedAmount = amount;
    }

    /**
     * Converts a given SavedAmount into this class for JAXB use.
     *
     * @param source future changes to this will not affect the created
     */
    public XmlAdaptedSavedAmount(SavedAmount source) {
        this.savedAmount = source.toString();
    }

    /**
     * Converts this jaxb-friendly adapted tag object into the wish's SavedAmount object.
     *
     * @throws IllegalValueException if there were any data constraints violated in the adapted wish
     */
    public SavedAmount toSavedAmountType() throws IllegalValueException {
        if (!SavedAmount.isValidSavedAmount(savedAmount)) {
            throw new IllegalValueException(SavedAmount.MESSAGE_SAVED_AMOUNT_INVALID);
        }
        return new SavedAmount(savedAmount);
    }

    @Override
    public boolean equals(Object other) {
        if (other == this) {
            return true;
        }

        if (!(other instanceof XmlAdaptedSavedAmount)) {
            return false;
        }

        return savedAmount.equals(((XmlAdaptedSavedAmount) other).savedAmount);
    }


}
<file_sep>/src/main/java/seedu/address/commons/events/ui/WishDataUpdatedEvent.java
package seedu.address.commons.events.ui;

import seedu.address.commons.events.BaseEvent;
import seedu.address.model.wish.Wish;

/**
 * Represents a selection change in the Wish List Panel
 */
public class WishDataUpdatedEvent extends BaseEvent {

    private final Wish newData;

    public WishDataUpdatedEvent(Wish newData) {
        this.newData = newData;
    }

    @Override
    public String toString() {
        return getClass().getSimpleName();
    }

    public Wish getNewData() {
        return newData;
    }
}
<file_sep>/src/main/java/seedu/address/logic/commands/SaveCommand.java
package seedu.address.logic.commands;

import static java.util.Objects.requireNonNull;
import static seedu.address.model.Model.PREDICATE_SHOW_ALL_WISHES;

import java.util.List;

import seedu.address.commons.core.EventsCenter;
import seedu.address.commons.core.Messages;
import seedu.address.commons.core.amount.Amount;
import seedu.address.commons.core.index.Index;
import seedu.address.commons.events.ui.WishDataUpdatedEvent;
import seedu.address.logic.CommandHistory;
import seedu.address.logic.commands.exceptions.CommandException;
import seedu.address.model.Model;
import seedu.address.model.wish.Wish;

/**
 * Saves specified amount to a specified wish.
 */
public class SaveCommand extends Command {
    public static final String COMMAND_WORD = "save";
    public static final String COMMAND_ALIAS = "sv";

    public static final String MESSAGE_USAGE = COMMAND_WORD + ": Channels saving amount to wish."
            + "Parameters: INDEX (must be a positive integer) "
            + "[SAVING_AMOUNT]\n"
            + "Example: " + COMMAND_WORD + " 1 "
            + "108.50";

    public static final String MESSAGE_SAVE_SUCCESS = "Saved %1$s for wish %2$s%3$s.";
    public static final String MESSAGE_SAVE_UNUSED_FUNDS = "Saved $%1$s to Unused Funds. "
            + "Unused Funds now contains $%2$s.";
    public static final String MESSAGE_SAVE_EXCESS = " with $%1$s in excess. Unused Funds now contains $%2$s";
    public static final String MESSAGE_SAVE_DIFFERENCE = " with $%1$s left to completion";

    private final Index index;
    private final Amount amountToSave;
    private final boolean noWishSpecified;

    public SaveCommand(Index index, Amount amountToSave) {
        requireNonNull(index);
        requireNonNull(amountToSave);

        this.index = index;
        this.amountToSave = amountToSave;
        this.noWishSpecified = false;
    }

    /**
     * This constructor is for a {@Code SaveCommand} that directs the amount to unusedFunds.
     * @param amountToSave
     */
    public SaveCommand(Amount amountToSave) {
        requireNonNull(amountToSave);

        this.index = null;
        this.amountToSave = amountToSave;
        this.noWishSpecified = true;
    }

    @Override
    public CommandResult execute(Model model, CommandHistory history) throws CommandException {
        List<Wish> lastShownList = model.getFilteredSortedWishList();

        if (this.noWishSpecified) {
            try {
                model.updateUnusedFunds(this.amountToSave);
                model.commitWishBook();
                return new CommandResult(String.format(MESSAGE_SAVE_UNUSED_FUNDS, amountToSave.toString(),
                        model.getUnusedFunds()));
            } catch (IllegalArgumentException iae) {
                throw new CommandException(iae.getMessage());
            }

        }

        if (index.getZeroBased() >= lastShownList.size()) {
            throw new CommandException(Messages.MESSAGE_INVALID_WISH_DISPLAYED_INDEX);
        }

        Wish wishToEdit = lastShownList.get(index.getZeroBased());
        if (wishToEdit.isFulfilled()) {
            throw new CommandException(Messages.MESSAGE_WISH_FULFILLED);
        }

        String differenceString = "";

        try {
            Wish editedWish = Wish.createWishWithIncrementedSavedAmount(wishToEdit, amountToSave);

            Amount wishSavedDifference = editedWish.getSavedAmountToPriceDifference();
            if (wishSavedDifference.value > 0) {
                Amount amountToIncrement = wishToEdit.getSavedAmountToPriceDifference().getAbsoluteAmount();
                editedWish = Wish.createWishWithIncrementedSavedAmount(wishToEdit, amountToIncrement);
                model.updateUnusedFunds(wishSavedDifference.getAbsoluteAmount());
                differenceString = String.format(MESSAGE_SAVE_EXCESS, wishSavedDifference.getAbsoluteAmount(),
                        model.getWishBook().getUnusedFunds());
            } else {
                differenceString = String.format(MESSAGE_SAVE_DIFFERENCE, wishSavedDifference.getAbsoluteAmount());
            }

            model.updateWish(wishToEdit, editedWish);
            model.updateFilteredWishList(PREDICATE_SHOW_ALL_WISHES);
            model.commitWishBook();
            EventsCenter.getInstance().post(new WishDataUpdatedEvent(editedWish));
        } catch (IllegalArgumentException iae) {
            throw new CommandException(iae.getMessage());
        }

        return new CommandResult(String.format(MESSAGE_SAVE_SUCCESS, amountToSave.toString(),
                index.getOneBased(), differenceString));
    }

    @Override
    public boolean equals(Object other) {
        if (other == this) {
            return true;
        }

        if (!(other instanceof SaveCommand)) {
            return false;
        }

        SaveCommand saveCommand = (SaveCommand) other;

        if (this.noWishSpecified) {
            return (this.index == null)
                    && this.amountToSave.equals(saveCommand.amountToSave)
                    && (this.noWishSpecified == saveCommand.noWishSpecified);
        }

        return this.index.equals(saveCommand.index)
                && this.amountToSave.equals(saveCommand.amountToSave)
                && (this.noWishSpecified == saveCommand.noWishSpecified);
    }
}
<file_sep>/docs/old_userguide.md
## User Guide

[1. Introduction](#section-1)

[2. Quick Start](#section-2)

[3. Features](#section-3)

[3.1. Viewing help: `help`](#section-3.1)

[3.2. Adding a wish: `add`](#section-3.2)

[3.3. Listing all wishes: `list`](#section-3.3)

[3.4. Viewing your saving history: `history`](#section-3.4)

[3.5. Editing a wish: `edit`](#section-3.5)

[3.6. Locating a wish: `find`](#section-3.6)

[3.7. Deleting a wish: `delete`](#section-3.7)

[3.8. Ranking your wishes: `rank`](#section-3.8)

[3.9. Reordering the priority of your wishes: `swap`](#section-3.9)

[3.10. Save money for a specific wish: `save`](#section-3.10)

[3.11. Undoing previous command: `undo`](#section-3.11)

[3.12. Redoing the previously undone command: `redo`](#section-3.12)

[3.13. Clearing all entries: `clear`](#section-3.13)

[3.14. Exiting the program: `exit`](#section-3.14)

[4. FAQ](#section-4)

[5. Command Summary](#section-5)

### 1. Introduction <a id="section-1"></a>
SaveGoals (SG) is a piggy bank in a digital app form, made for people who need a user-friendly solution for keeping track of their disposable income. Not only does SG keep records of your savings, it also allows users to set items as goals for users to work towards, serving as a way to cultivate the habit of saving. SG is made for people who have material wants in life, but are uncertain of purchasing said wants, by helping them to reserve disposable income so that they know they can spend on their wants. So what are you waiting for? Go to Section 2, “Quick Start” and start saving!

### 2. Quick Start <a id="section-2"></a>
1. Ensure you have Java version 9 or later installed on your Computer.
2. Download the latest [savegoals.jar]() here.
3. Copy the file to the folder you want to use as the home folder for your Save Goals app.
4. Double-click the .jar file to start the app. The GUI should appear in a few seconds.
image::Ui.png[width="790"]
5. Type the command in the command box and press Enter to execute it.
    *  Typing help and pressing enter will bring up a help window.
6. Here are some commands that you can try:
    * `list`: Lists all the items you have set as wishes (sorted by due date).
    * `save AMOUNT`
        * `save 20.50`: adds $20.50 to your nearest goal that’s in-progress.
    * `add n/WISH_NAME p/PRICE t/[d/TIME_GIVEN]/[d/END_DATE]`
        * `add n/uPhoneXX p/1000 d/5m`:  adds an item “uPhoneXX” as a goal to be completed in 5 months.
        * `add n/uPhoneXX p/1000 d/12112018`:  adds an item “uPhoneXX” as a goal that costs $1000 and sets it to be completed by 12-11-2018.
    * `clear`: clears view.
    * `help`: displays list of command with usage.
    * `exit`: exits the app command.
7. Refer to [Section 3, “Features”](#section-3) for details of each command.

### 3. Features <a id="section-3"></a>

---
#### Command Format
>* Words in `UPPER_CASE` are the parameters to be supplied by the user
    * e.g. in `add WISH`, `WISH` is a parameter which can be used as add iPhone.
* Items in square brackets are optional
    * e.g. in `save AMOUNT [INDEX]`, `INDEX` is an optional parameter, since the save command can be used as `save 40`.
* The `/` symbol between parameters means that you can use either of the parameters types in the command
    * e.g. in `add WISH PRICE [TIME_GIVEN]/[START_DATE to END_DATE]`, you provide either the `TIME_GIVEN` parameter, `START_DATE` and `END_DATE` parameters, or none of them.
>* Items with `...` after them can be used multiple times.
---

#### 3.1 Viewing help: `help` <a id="section-3.1"></a>
Format: `help`


#### 3.2 Adding a wish: `add` <a id="section-3.2"></a>
Adds a wish to your wish list
Format: `add n/WISH_NAME p/PRICE t/[d/TIME_GIVEN]/[d/END_DATE]`

---
>* `[END_DATE]`: Specified in _ddmmyyyy_ format. Separators are optional. Allowed separators are either dashes ‘-’, forward slashes ‘/’ or periods ‘.’
>* `[TIME_GIVEN]`: Specified in terms of days, weeks or months or years, prefixes marking such time periods are _‘d’, ‘w’, ‘m’_ and _‘y’_ respectively.
---

Examples:
* `add n/smallRice p/999 d/2d`
* `add n/kfcBook_13inch p/2300 d/6m3w`
* `add n/prinkles p/1.95 d/24/04/2020`


#### 3.3 Listing all wishes: `list` <a id="section-3.3"></a>
Shows a list of all the goals you have set, sorted by date by default.
Format: `list`


#### 3.4. Viewing your saving history: `history` <a id="section-3.4"></a>
Shows a history of wishes you have accomplished, from newest to oldest.
Format: `history`


#### 3.5 Editing a wish: `edit` <a id="section-3.5"></a>
Edits an existing wish in the list
Format: `edit INDEX [n/WISH_NAME] [p/PRICE] [d/TIME_GIVEN]/[d/END_DATE] [t/TAG]`

---
>* Edits the wish at the specified `INDEX`. `INDEX` refers to the index number shown in the displayed list of goals. `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 wish will be removed i.e. adding of tags is not cumulative.
>* You can remove all tags by typing `t/` without specifying any tags after it.
---

Examples:
* `edit 1 n/Macbook Pro t/Broke wishes`
Edits the name of the wish and the tag of the 1st goal to be Macbook Pro and Broke wishes respectively
* `edit 2 p/22 d/22w`
Edits the price and time given to accomplish the 2nd goal to 22 (in the chosen currency) and 22 weeks respectively.


#### 3.6 Locating a wish: `find` <a id="section-3.6"></a>
Finds wishes which satisfy the given search predicate.
Format: `find SEARCH_PREDICATE [MORE_SEARCH_PREDICATES]`

---
>* The user can search using the following search predicates:
    * `NAME`
    * `DATE`
    * `PRICE`
    * `TAG`
* `NAME`is the default search predicate.
* The search is case insensitive e.g. watch will match Watch.
* Only full words will be matched e.g. wat will not match watch.
* Goals matching at least one keyword will be returned e.g. watch will return apple watch, pebble watch.
* `DATE` is the creation date of the wish. It should match the correct format as specified in [Section 3.2 Date Format]().
>* `PRICE` is the sale price of the item. It should be a positive number corrected to the smallest denomination of the currency.
---

Examples:
* `find 22d`
Returns wish with stipulated time given of 22 days.
* `find watch t/broke wishes`
Returns any wish with name containing watch, with tag broke wishes.


#### 3.7 Deleting a wish: `delete` <a id="section-3.7"></a>
Deletes the specified wish from the list.
Format: `delete INDEX`

---
>* `INDEX` refers to the index number shown in the displayed list.
>* `INDEX` must be a positive integer 1, 2, 3...
---

Examples:
* `list`
`delete 2`
Deletes the 2nd wish in the list.
* `find watch`
`delete 1`
Deletes the 1st wish in the results of the find command (if any).


#### 3.8 Ranking your wishes: `rank` <a id="section-3.8"></a>
Ranks the wishes by specified wish field so that future savings are allocated in the order or ranking.
Format: `rank WISH_FIELD [RANK_ORDER]`

---
> `RANK_ORDER` can be `-a`(ascending) or `-d`(descending). By default it is set to ascending.
---

Examples:
* `rank Date -d`
Ranks the wishes in descending order of date created.
* `rank Price`
Ranks the wishes in ascending order of sale price


#### 3.9 Reordering the priority of your wishes: `swap` <a id="section-3.9"></a>
Reorders the priority of wishes by swapping wishes at the specified indices.
Format: `reorder OLD_INDEX NEW_INDEX `

---
>* A smaller numerical value for index indicates higher priority.
>* Indices must be a positive integer 1, 2, 3...
---

Examples:
* `swap 7 1`
Swaps wishes at index 7 and index 1
* `swap 1 8`
Swaps wishes at index 1 and index 8


#### 3.10 Save money for a specific wish: `save` <a id="section-3.10"></a>
Channels savings for a specified wish.
Format: `save AMOUNT [INDEX]`

---
>* `INDEX` should be a positive integer 1, 2, 3…
* `AMOUNT` should be a positive value to the smallest denomination of the currency.
>* If no `INDEX` is specified, money will be transferred to the wish which has the closest due date.
---

Examples:
* `save 1000 1`
Adds $1000 into the item at index 1.


#### 3.11 Undoing previous command: `undo` <a id="section-3.11"></a>
Restores SaveGoals to the state before the previous undoable command was executed.
Format: `undo`

---
> Undoable commands: commands that modify SaveGoals content (`add, delete, edit, save, rank`)
---

Examples:
* `delete 1`
`list `
`undo`
Reverses the `delete 1` command.
* `list`
`find 22d`
`undo`
The `undo` command fails as there are no undoable commands executed previously.
* `delete 1`
`clear`
`undo`
`undo`
The first `undo` reverses the `clear` command. The second `undo` reverses the `delete 1` command.

#### 3.12 Redoing the previously undone command: `redo` <a id="section-3.12"></a>
Reverses the most recent `undo` command.
Format: `redo`

Examples:
* `delete 1`
`list`
`undo`
`redo`
`undo` reverses the `delete 1` command. Then, the `redo` command reapplies the `delete 1` command.
* `delete 1`
`redo`
The `redo` command fails as there are no `undo` commands executed previously.


#### 3.13 Clearing all entries: `clear` <a id="section-3.13"></a>
Clears all entries from SaveGoals.
Format: `clear`


#### 3.14 Exiting the program: `exit` <a id="section-3.14"></a>
Exits the program.
Format: `exit`


### 4. FAQ <a id="section-4"></a>
* **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 SaveGoals folder.

### 5. Command Summary <a id="section-5"></a>
* Help: `help`
* Add: `add n/WISH_NAME p/PRICE t/[d/TIME_GIVEN]/[d/END_DATE]`
e.g. add n/kfcBook_13inch p/2300 d/6m3w
* List: `list`
* History: `history`
* Edit: `edit INDEX [n/WISH_NAME] [p/PRICE] [d/TIME_GIVEN]/[d/END_DATE] [t/TAG]`
e.g. edit 1 n/Macbook Pro t/Broke wishes
* Find: `find SEARCH_PREDICATE [MORE_SEARCH_PREDICATES]`
e.g. find 22d
* Delete: `delete INDEX`
e.g. delete 1
* Rank: `rank WISH_FIELD [RANK_ORDER]`
e.g. rank Date -d
* Swap: `swap OLD_INDEX NEW_INDEX`
e.g. swap 1 8
* Save: `save AMOUNT [INDEX]`
e.g. save 1000 1
* Undo: `undo`
* Redo: `redo`
* Clear: `clear`
* Exit: `exit`
<file_sep>/src/main/java/seedu/address/model/versionedmodels/VersionedWishBook.java
package seedu.address.model.versionedmodels;

import java.util.ArrayList;
import java.util.List;

import seedu.address.model.ReadOnlyWishBook;
import seedu.address.model.WishBook;

/**
 * {@code WishBook} that keeps track of its own history.
 */
public class VersionedWishBook extends WishBook implements VersionedModel {

    private final List<ReadOnlyWishBook> wishBookStateList;
    private int currentStatePointer;

    public VersionedWishBook(ReadOnlyWishBook initialState) {
        super(initialState);

        wishBookStateList = new ArrayList<>();
        wishBookStateList.add(new WishBook(initialState));
        currentStatePointer = 0;
    }

    /**
     * Saves a copy of the current {@code WishBook} state at the end of the state list.
     * Undone states are removed from the state list.
     */
    @Override
    public void commit() {
        removeStatesAfterCurrentPointer();
        wishBookStateList.add(new WishBook(this));
        currentStatePointer++;
    }

    private void removeStatesAfterCurrentPointer() {
        wishBookStateList.subList(currentStatePointer + 1, wishBookStateList.size()).clear();
    }

    /**
     * Restores the address book to its previous state.
     */
    @Override
    public void undo() {
        if (!canUndo()) {
            throw new NoUndoableStateException();
        }
        currentStatePointer--;
        resetData(wishBookStateList.get(currentStatePointer));
    }

    /**
     * Restores the address book to its previously undone state.
     */
    @Override
    public void redo() {
        if (!canRedo()) {
            throw new NoRedoableStateException();
        }
        currentStatePointer++;
        resetData(wishBookStateList.get(currentStatePointer));
    }

    /**
     * Returns true if {@code undo()} has address book states to undo.
     */
    public boolean canUndo() {
        return currentStatePointer > 0;
    }

    /**
     * Returns true if {@code redo()} has address book states to redo.
     */
    public boolean canRedo() {
        return currentStatePointer < wishBookStateList.size() - 1;
    }

    @Override
    public boolean equals(Object other) {
        // short circuit if same object
        if (other == this) {
            return true;
        }

        // instanceof handles nulls
        if (!(other instanceof VersionedWishBook)) {
            return false;
        }

        VersionedWishBook otherVersionedWishBook = (VersionedWishBook) other;

        // state check
        return super.equals(otherVersionedWishBook)
                && wishBookStateList.equals(otherVersionedWishBook.wishBookStateList)
                && currentStatePointer == otherVersionedWishBook.currentStatePointer;
    }

}
<file_sep>/src/main/java/seedu/address/logic/commands/EditCommand.java
package seedu.address.logic.commands;

import static java.util.Objects.requireNonNull;
import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE;
import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME;
import static seedu.address.logic.parser.CliSyntax.PREFIX_PRICE;
import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG;
import static seedu.address.logic.parser.CliSyntax.PREFIX_URL;
import static seedu.address.model.Model.PREDICATE_SHOW_ALL_WISHES;

import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;

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.tag.Tag;
import seedu.address.model.wish.Date;
import seedu.address.model.wish.Name;
import seedu.address.model.wish.Price;
import seedu.address.model.wish.Remark;
import seedu.address.model.wish.SavedAmount;
import seedu.address.model.wish.Url;
import seedu.address.model.wish.Wish;

/**
 * Edits the details of an existing wish in the address book.
 */
public class EditCommand extends Command {

    public static final String COMMAND_WORD = "edit";
    public static final String COMMAND_ALIAS = "e";

    public static final String MESSAGE_USAGE = COMMAND_WORD + ": Edits the details of the wish identified "
            + "by the index number used in the displayed wish list. "
            + "Existing values will be overwritten by the input values.\n"
            + "Parameters: INDEX (must be a positive integer) "
            + "[" + PREFIX_NAME + "NAME] "
            + "[" + PREFIX_PRICE + "PRICE] "
            + "[" + PREFIX_DATE + "DATE] "
            + "[" + PREFIX_URL + "URL] "
            + "[" + PREFIX_TAG + "TAG]...\n"
            + "Example: " + COMMAND_WORD + " 1 "
            + PREFIX_PRICE + "60.60 "
            + PREFIX_DATE + "20/11/2021";

    public static final String MESSAGE_EDIT_WISH_SUCCESS = "Edited Wish: %1$s";
    public static final String MESSAGE_NOT_EDITED = "At least one field to edit must be provided.";
    public static final String MESSAGE_DUPLICATE_WISH = "This wish already exists in the wish book.";

    private final Index index;
    private final EditWishDescriptor editWishDescriptor;

    /**
     * @param index of the wish in the filtered wish list to edit
     * @param editWishDescriptor details to edit the wish with
     */
    public EditCommand(Index index, EditWishDescriptor editWishDescriptor) {
        requireNonNull(index);
        requireNonNull(editWishDescriptor);

        this.index = index;
        this.editWishDescriptor = new EditWishDescriptor(editWishDescriptor);
    }

    @Override
    public CommandResult execute(Model model, CommandHistory history) throws CommandException {
        requireNonNull(model);
        List<Wish> lastShownList = model.getFilteredSortedWishList();

        if (index.getZeroBased() >= lastShownList.size()) {
            throw new CommandException(Messages.MESSAGE_INVALID_WISH_DISPLAYED_INDEX);
        }

        Wish wishToEdit = lastShownList.get(index.getZeroBased());
        Wish editedWish = createEditedWish(wishToEdit, editWishDescriptor);

        if (!wishToEdit.isSameWish(editedWish) && model.hasWish(editedWish)) {
            throw new CommandException(MESSAGE_DUPLICATE_WISH);
        }

        model.updateWish(wishToEdit, editedWish);
        model.updateFilteredWishList(PREDICATE_SHOW_ALL_WISHES);
        model.commitWishBook();
        return new CommandResult(String.format(MESSAGE_EDIT_WISH_SUCCESS, editedWish));
    }

    /**
     * Creates and returns a {@code Wish} with the details of {@code wishToEdit}
     * edited with {@code editWishDescriptor}.
     */
    private static Wish createEditedWish(Wish wishToEdit, EditWishDescriptor editWishDescriptor) {
        assert wishToEdit != null;

        Name updatedName = editWishDescriptor.getName().orElse(wishToEdit.getName());
        Price updatedPrice = editWishDescriptor.getPrice().orElse(wishToEdit.getPrice());
        Date updatedDate = editWishDescriptor.getDate().orElse(wishToEdit.getDate());
        Url updatedUrl = editWishDescriptor.getUrl().orElse(wishToEdit.getUrl());
        SavedAmount savedAmount = wishToEdit.getSavedAmount(); // edit command does not allow editing remarks
        Remark remark = wishToEdit.getRemark(); // cannot modify remark with edit command
        Set<Tag> updatedTags = editWishDescriptor.getTags().orElse(wishToEdit.getTags());
        UUID originalUuid = wishToEdit.getId();

        return new Wish(updatedName, updatedPrice, updatedDate, updatedUrl,
                savedAmount, remark, updatedTags, originalUuid);
    }

    @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)
                && editWishDescriptor.equals(e.editWishDescriptor);
    }

    /**
     * Stores the details to edit the wish with. Each non-empty field value will replace the
     * corresponding field value of the wish.
     */
    public static class EditWishDescriptor {
        private Name name;
        private Price price;
        private Date date;
        private Url url;
        private Set<Tag> tags;

        public EditWishDescriptor() {}

        /**
         * Copy constructor.
         * A defensive copy of {@code tags} is used internally.
         */
        public EditWishDescriptor(EditWishDescriptor toCopy) {
            setName(toCopy.name);
            setPrice(toCopy.price);
            setDate(toCopy.date);
            setUrl(toCopy.url);
            setTags(toCopy.tags);
        }

        /**
         * Returns true if at least one field is edited.
         */
        public boolean isAnyFieldEdited() {
            return CollectionUtil.isAnyNonNull(name, price, date, url, tags);
        }

        public void setName(Name name) {
            this.name = name;
        }

        public Optional<Name> getName() {
            return Optional.ofNullable(name);
        }

        public void setPrice(Price price) {
            this.price = price;
        }

        public Optional<Price> getPrice() {
            return Optional.ofNullable(price);
        }

        public void setDate(Date date) {
            this.date = date;
        }

        public Optional<Date> getDate() {
            return Optional.ofNullable(date);
        }

        public void setUrl(Url url) {
            this.url = url;
        }

        public Optional<Url> getUrl() {
            return Optional.ofNullable(url);
        }

        /**
         * Sets {@code tags} to this object's {@code tags}.
         * A defensive copy of {@code tags} is used internally.
         */
        public void setTags(Set<Tag> 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<Set<Tag>> 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 EditWishDescriptor)) {
                return false;
            }

            // state check
            EditWishDescriptor e = (EditWishDescriptor) other;

            return getName().equals(e.getName())
                    && getPrice().equals(e.getPrice())
                    && getDate().equals(e.getDate())
                    && getUrl().equals(e.getUrl())
                    && getTags().equals(e.getTags());
        }
    }
}
<file_sep>/docs/team/dioptrique.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<!--[if IE]><meta http-equiv="X-UA-Compatible" content="IE=edge"><![endif]-->
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="generator" content="Asciidoctor 1.5.8">
<title><NAME> - Project Portfolio</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Open+Sans:300,300italic,400,400italic,600,600italic%7CNoto+Serif:400,400italic,700,700italic%7CDroid+Sans+Mono:400,700">
<style>
/* Asciidoctor default stylesheet | MIT License | http://asciidoctor.org */
/* Uncomment @import statement below to use as custom stylesheet */
/*@import "https://fonts.googleapis.com/css?family=Open+Sans:300,300italic,400,400italic,600,600italic%7CNoto+Serif:400,400italic,700,700italic%7CDroid+Sans+Mono:400,700";*/
article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}
audio,canvas,video{display:inline-block}
audio:not([controls]){display:none;height:0}
script{display:none!important}
html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}
a{background:transparent}
a:focus{outline:thin dotted}
a:active,a:hover{outline:0}
h1{font-size:2em;margin:.67em 0}
abbr[title]{border-bottom:1px dotted}
b,strong{font-weight:bold}
dfn{font-style:italic}
hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}
mark{background:#ff0;color:#000}
code,kbd,pre,samp{font-family:monospace;font-size:1em}
pre{white-space:pre-wrap}
q{quotes:"\201C" "\201D" "\2018" "\2019"}
small{font-size:80%}
sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}
sup{top:-.5em}
sub{bottom:-.25em}
img{border:0}
svg:not(:root){overflow:hidden}
figure{margin:0}
fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}
legend{border:0;padding:0}
button,input,select,textarea{font-family:inherit;font-size:100%;margin:0}
button,input{line-height:normal}
button,select{text-transform:none}
button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}
button[disabled],html input[disabled]{cursor:default}
input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0}
button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}
textarea{overflow:auto;vertical-align:top}
table{border-collapse:collapse;border-spacing:0}
*,*::before,*::after{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}
html,body{font-size:100%}
body{background:#fff;color:rgba(0,0,0,.8);padding:0;margin:0;font-family:"Noto Serif","DejaVu Serif",serif;font-weight:400;font-style:normal;line-height:1;position:relative;cursor:auto;tab-size:4;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased}
a:hover{cursor:pointer}
img,object,embed{max-width:100%;height:auto}
object,embed{height:100%}
img{-ms-interpolation-mode:bicubic}
.left{float:left!important}
.right{float:right!important}
.text-left{text-align:left!important}
.text-right{text-align:right!important}
.text-center{text-align:center!important}
.text-justify{text-align:justify!important}
.hide{display:none}
img,object,svg{display:inline-block;vertical-align:middle}
textarea{height:auto;min-height:50px}
select{width:100%}
.center{margin-left:auto;margin-right:auto}
.stretch{width:100%}
.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{line-height:1.45;color:#7a2518;font-weight:400;margin-top:0;margin-bottom:.25em}
div,dl,dt,dd,ul,ol,li,h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6,pre,form,p,blockquote,th,td{margin:0;padding:0;direction:ltr}
a{color:#2156a5;text-decoration:underline;line-height:inherit}
a:hover,a:focus{color:#1d4b8f}
a img{border:none}
p{font-family:inherit;font-weight:400;font-size:1em;line-height:1.6;margin-bottom:1.25em;text-rendering:optimizeLegibility}
p aside{font-size:.875em;line-height:1.35;font-style:italic}
h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{font-family:"Open Sans","DejaVu Sans",sans-serif;font-weight:300;font-style:normal;color:#ba3925;text-rendering:optimizeLegibility;margin-top:1em;margin-bottom:.5em;line-height:1.0125em}
h1 small,h2 small,h3 small,#toctitle small,.sidebarblock>.content>.title small,h4 small,h5 small,h6 small{font-size:60%;color:#e99b8f;line-height:0}
h1{font-size:2.125em}
h2{font-size:1.6875em}
h3,#toctitle,.sidebarblock>.content>.title{font-size:1.375em}
h4,h5{font-size:1.125em}
h6{font-size:1em}
hr{border:solid #dddddf;border-width:1px 0 0;clear:both;margin:1.25em 0 1.1875em;height:0}
em,i{font-style:italic;line-height:inherit}
strong,b{font-weight:bold;line-height:inherit}
small{font-size:60%;line-height:inherit}
code{font-family:"Droid Sans Mono","DejaVu Sans Mono",monospace;font-weight:400;color:rgba(0,0,0,.9)}
ul,ol,dl{font-size:1em;line-height:1.6;margin-bottom:1.25em;list-style-position:outside;font-family:inherit}
ul,ol{margin-left:1.5em}
ul li ul,ul li ol{margin-left:1.25em;margin-bottom:0;font-size:1em}
ul.square li ul,ul.circle li ul,ul.disc li ul{list-style:inherit}
ul.square{list-style-type:square}
ul.circle{list-style-type:circle}
ul.disc{list-style-type:disc}
ol li ul,ol li ol{margin-left:1.25em;margin-bottom:0}
dl dt{margin-bottom:.3125em;font-weight:bold}
dl dd{margin-bottom:1.25em}
abbr,acronym{text-transform:uppercase;font-size:90%;color:rgba(0,0,0,.8);border-bottom:1px dotted #ddd;cursor:help}
abbr{text-transform:none}
blockquote{margin:0 0 1.25em;padding:.5625em 1.25em 0 1.1875em;border-left:1px solid #ddd}
blockquote cite{display:block;font-size:.9375em;color:rgba(0,0,0,.6)}
blockquote cite::before{content:"\2014 \0020"}
blockquote cite a,blockquote cite a:visited{color:rgba(0,0,0,.6)}
blockquote,blockquote p{line-height:1.6;color:rgba(0,0,0,.85)}
@media screen and (min-width:768px){h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{line-height:1.2}
h1{font-size:2.75em}
h2{font-size:2.3125em}
h3,#toctitle,.sidebarblock>.content>.title{font-size:1.6875em}
h4{font-size:1.4375em}}
table{background:#fff;margin-bottom:1.25em;border:solid 1px #dedede}
table thead,table tfoot{background:#f7f8f7}
table thead tr th,table thead tr td,table tfoot tr th,table tfoot tr td{padding:.5em .625em .625em;font-size:inherit;color:rgba(0,0,0,.8);text-align:left}
table tr th,table tr td{padding:.5625em .625em;font-size:inherit;color:rgba(0,0,0,.8)}
table tr.even,table tr.alt,table tr:nth-of-type(even){background:#f8f8f7}
table thead tr th,table tfoot tr th,table tbody tr td,table tr td,table tfoot tr td{display:table-cell;line-height:1.6}
h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{line-height:1.2;word-spacing:-.05em}
h1 strong,h2 strong,h3 strong,#toctitle strong,.sidebarblock>.content>.title strong,h4 strong,h5 strong,h6 strong{font-weight:400}
.clearfix::before,.clearfix::after,.float-group::before,.float-group::after{content:" ";display:table}
.clearfix::after,.float-group::after{clear:both}
*:not(pre)>code{font-size:.9375em;font-style:normal!important;letter-spacing:0;padding:.1em .5ex;word-spacing:-.15em;background-color:#f7f7f8;-webkit-border-radius:4px;border-radius:4px;line-height:1.45;text-rendering:optimizeSpeed;word-wrap:break-word}
*:not(pre)>code.nobreak{word-wrap:normal}
*:not(pre)>code.nowrap{white-space:nowrap}
pre,pre>code{line-height:1.45;color:rgba(0,0,0,.9);font-family:"Droid Sans Mono","DejaVu Sans Mono",monospace;font-weight:400;text-rendering:optimizeSpeed}
em em{font-style:normal}
strong strong{font-weight:400}
.keyseq{color:rgba(51,51,51,.8)}
kbd{font-family:"Droid Sans Mono","DejaVu Sans Mono",monospace;display:inline-block;color:rgba(0,0,0,.8);font-size:.65em;line-height:1.45;background-color:#f7f7f7;border:1px solid #ccc;-webkit-border-radius:3px;border-radius:3px;-webkit-box-shadow:0 1px 0 rgba(0,0,0,.2),0 0 0 .1em white inset;box-shadow:0 1px 0 rgba(0,0,0,.2),0 0 0 .1em #fff inset;margin:0 .15em;padding:.2em .5em;vertical-align:middle;position:relative;top:-.1em;white-space:nowrap}
.keyseq kbd:first-child{margin-left:0}
.keyseq kbd:last-child{margin-right:0}
.menuseq,.menuref{color:#000}
.menuseq b:not(.caret),.menuref{font-weight:inherit}
.menuseq{word-spacing:-.02em}
.menuseq b.caret{font-size:1.25em;line-height:.8}
.menuseq i.caret{font-weight:bold;text-align:center;width:.45em}
b.button::before,b.button::after{position:relative;top:-1px;font-weight:400}
b.button::before{content:"[";padding:0 3px 0 2px}
b.button::after{content:"]";padding:0 2px 0 3px}
p a>code:hover{color:rgba(0,0,0,.9)}
#header,#content,#footnotes,#footer{width:100%;margin-left:auto;margin-right:auto;margin-top:0;margin-bottom:0;max-width:62.5em;*zoom:1;position:relative;padding-left:.9375em;padding-right:.9375em}
#header::before,#header::after,#content::before,#content::after,#footnotes::before,#footnotes::after,#footer::before,#footer::after{content:" ";display:table}
#header::after,#content::after,#footnotes::after,#footer::after{clear:both}
#content{margin-top:1.25em}
#content::before{content:none}
#header>h1:first-child{color:rgba(0,0,0,.85);margin-top:2.25rem;margin-bottom:0}
#header>h1:first-child+#toc{margin-top:8px;border-top:1px solid #dddddf}
#header>h1:only-child,body.toc2 #header>h1:nth-last-child(2){border-bottom:1px solid #dddddf;padding-bottom:8px}
#header .details{border-bottom:1px solid #dddddf;line-height:1.45;padding-top:.25em;padding-bottom:.25em;padding-left:.25em;color:rgba(0,0,0,.6);display:-ms-flexbox;display:-webkit-flex;display:flex;-ms-flex-flow:row wrap;-webkit-flex-flow:row wrap;flex-flow:row wrap}
#header .details span:first-child{margin-left:-.125em}
#header .details span.email a{color:rgba(0,0,0,.85)}
#header .details br{display:none}
#header .details br+span::before{content:"\00a0\2013\00a0"}
#header .details br+span.author::before{content:"\00a0\22c5\00a0";color:rgba(0,0,0,.85)}
#header .details br+span#revremark::before{content:"\00a0|\00a0"}
#header #revnumber{text-transform:capitalize}
#header #revnumber::after{content:"\00a0"}
#content>h1:first-child:not([class]){color:rgba(0,0,0,.85);border-bottom:1px solid #dddddf;padding-bottom:8px;margin-top:0;padding-top:1rem;margin-bottom:1.25rem}
#toc{border-bottom:1px solid #e7e7e9;padding-bottom:.5em}
#toc>ul{margin-left:.125em}
#toc ul.sectlevel0>li>a{font-style:italic}
#toc ul.sectlevel0 ul.sectlevel1{margin:.5em 0}
#toc ul{font-family:"Open Sans","DejaVu Sans",sans-serif;list-style-type:none}
#toc li{line-height:1.3334;margin-top:.3334em}
#toc a{text-decoration:none}
#toc a:active{text-decoration:underline}
#toctitle{color:#7a2518;font-size:1.2em}
@media screen and (min-width:768px){#toctitle{font-size:1.375em}
body.toc2{padding-left:15em;padding-right:0}
#toc.toc2{margin-top:0!important;background-color:#f8f8f7;position:fixed;width:15em;left:0;top:0;border-right:1px solid #e7e7e9;border-top-width:0!important;border-bottom-width:0!important;z-index:1000;padding:1.25em 1em;height:100%;overflow:auto}
#toc.toc2 #toctitle{margin-top:0;margin-bottom:.8rem;font-size:1.2em}
#toc.toc2>ul{font-size:.9em;margin-bottom:0}
#toc.toc2 ul ul{margin-left:0;padding-left:1em}
#toc.toc2 ul.sectlevel0 ul.sectlevel1{padding-left:0;margin-top:.5em;margin-bottom:.5em}
body.toc2.toc-right{padding-left:0;padding-right:15em}
body.toc2.toc-right #toc.toc2{border-right-width:0;border-left:1px solid #e7e7e9;left:auto;right:0}}
@media screen and (min-width:1280px){body.toc2{padding-left:20em;padding-right:0}
#toc.toc2{width:20em}
#toc.toc2 #toctitle{font-size:1.375em}
#toc.toc2>ul{font-size:.95em}
#toc.toc2 ul ul{padding-left:1.25em}
body.toc2.toc-right{padding-left:0;padding-right:20em}}
#content #toc{border-style:solid;border-width:1px;border-color:#e0e0dc;margin-bottom:1.25em;padding:1.25em;background:#f8f8f7;-webkit-border-radius:4px;border-radius:4px}
#content #toc>:first-child{margin-top:0}
#content #toc>:last-child{margin-bottom:0}
#footer{max-width:100%;background-color:rgba(0,0,0,.8);padding:1.25em}
#footer-text{color:rgba(255,255,255,.8);line-height:1.44}
#content{margin-bottom:.625em}
.sect1{padding-bottom:.625em}
@media screen and (min-width:768px){#content{margin-bottom:1.25em}
.sect1{padding-bottom:1.25em}}
.sect1:last-child{padding-bottom:0}
.sect1+.sect1{border-top:1px solid #e7e7e9}
#content h1>a.anchor,h2>a.anchor,h3>a.anchor,#toctitle>a.anchor,.sidebarblock>.content>.title>a.anchor,h4>a.anchor,h5>a.anchor,h6>a.anchor{position:absolute;z-index:1001;width:1.5ex;margin-left:-1.5ex;display:block;text-decoration:none!important;visibility:hidden;text-align:center;font-weight:400}
#content h1>a.anchor::before,h2>a.anchor::before,h3>a.anchor::before,#toctitle>a.anchor::before,.sidebarblock>.content>.title>a.anchor::before,h4>a.anchor::before,h5>a.anchor::before,h6>a.anchor::before{content:"\00A7";font-size:.85em;display:block;padding-top:.1em}
#content h1:hover>a.anchor,#content h1>a.anchor:hover,h2:hover>a.anchor,h2>a.anchor:hover,h3:hover>a.anchor,#toctitle:hover>a.anchor,.sidebarblock>.content>.title:hover>a.anchor,h3>a.anchor:hover,#toctitle>a.anchor:hover,.sidebarblock>.content>.title>a.anchor:hover,h4:hover>a.anchor,h4>a.anchor:hover,h5:hover>a.anchor,h5>a.anchor:hover,h6:hover>a.anchor,h6>a.anchor:hover{visibility:visible}
#content h1>a.link,h2>a.link,h3>a.link,#toctitle>a.link,.sidebarblock>.content>.title>a.link,h4>a.link,h5>a.link,h6>a.link{color:#ba3925;text-decoration:none}
#content h1>a.link:hover,h2>a.link:hover,h3>a.link:hover,#toctitle>a.link:hover,.sidebarblock>.content>.title>a.link:hover,h4>a.link:hover,h5>a.link:hover,h6>a.link:hover{color:#a53221}
.audioblock,.imageblock,.literalblock,.listingblock,.stemblock,.videoblock{margin-bottom:1.25em}
.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{text-rendering:optimizeLegibility;text-align:left;font-family:"Noto Serif","DejaVu Serif",serif;font-size:1rem;font-style:italic}
table.tableblock.fit-content>caption.title{white-space:nowrap;width:0}
.paragraph.lead>p,#preamble>.sectionbody>[class="paragraph"]:first-of-type p{font-size:1.21875em;line-height:1.6;color:rgba(0,0,0,.85)}
table.tableblock #preamble>.sectionbody>[class="paragraph"]:first-of-type p{font-size:inherit}
.admonitionblock>table{border-collapse:separate;border:0;background:none;width:100%}
.admonitionblock>table td.icon{text-align:center;width:80px}
.admonitionblock>table td.icon img{max-width:none}
.admonitionblock>table td.icon .title{font-weight:bold;font-family:"Open Sans","DejaVu Sans",sans-serif;text-transform:uppercase}
.admonitionblock>table td.content{padding-left:1.125em;padding-right:1.25em;border-left:1px solid #dddddf;color:rgba(0,0,0,.6)}
.admonitionblock>table td.content>:last-child>:last-child{margin-bottom:0}
.exampleblock>.content{border-style:solid;border-width:1px;border-color:#e6e6e6;margin-bottom:1.25em;padding:1.25em;background:#fff;-webkit-border-radius:4px;border-radius:4px}
.exampleblock>.content>:first-child{margin-top:0}
.exampleblock>.content>:last-child{margin-bottom:0}
.sidebarblock{border-style:solid;border-width:1px;border-color:#e0e0dc;margin-bottom:1.25em;padding:1.25em;background:#f8f8f7;-webkit-border-radius:4px;border-radius:4px}
.sidebarblock>:first-child{margin-top:0}
.sidebarblock>:last-child{margin-bottom:0}
.sidebarblock>.content>.title{color:#7a2518;margin-top:0;text-align:center}
.exampleblock>.content>:last-child>:last-child,.exampleblock>.content .olist>ol>li:last-child>:last-child,.exampleblock>.content .ulist>ul>li:last-child>:last-child,.exampleblock>.content .qlist>ol>li:last-child>:last-child,.sidebarblock>.content>:last-child>:last-child,.sidebarblock>.content .olist>ol>li:last-child>:last-child,.sidebarblock>.content .ulist>ul>li:last-child>:last-child,.sidebarblock>.content .qlist>ol>li:last-child>:last-child{margin-bottom:0}
.literalblock pre,.listingblock pre:not(.highlight),.listingblock pre[class="highlight"],.listingblock pre[class^="highlight "],.listingblock pre.CodeRay,.listingblock pre.prettyprint{background:#f7f7f8}
.sidebarblock .literalblock pre,.sidebarblock .listingblock pre:not(.highlight),.sidebarblock .listingblock pre[class="highlight"],.sidebarblock .listingblock pre[class^="highlight "],.sidebarblock .listingblock pre.CodeRay,.sidebarblock .listingblock pre.prettyprint{background:#f2f1f1}
.literalblock pre,.literalblock pre[class],.listingblock pre,.listingblock pre[class]{-webkit-border-radius:4px;border-radius:4px;word-wrap:break-word;overflow-x:auto;padding:1em;font-size:.8125em}
@media screen and (min-width:768px){.literalblock pre,.literalblock pre[class],.listingblock pre,.listingblock pre[class]{font-size:.90625em}}
@media screen and (min-width:1280px){.literalblock pre,.literalblock pre[class],.listingblock pre,.listingblock pre[class]{font-size:1em}}
.literalblock pre.nowrap,.literalblock pre.nowrap pre,.listingblock pre.nowrap,.listingblock pre.nowrap pre{white-space:pre;word-wrap:normal}
.literalblock.output pre{color:#f7f7f8;background-color:rgba(0,0,0,.9)}
.listingblock pre.highlightjs{padding:0}
.listingblock pre.highlightjs>code{padding:1em;-webkit-border-radius:4px;border-radius:4px}
.listingblock pre.prettyprint{border-width:0}
.listingblock>.content{position:relative}
.listingblock code[data-lang]::before{display:none;content:attr(data-lang);position:absolute;font-size:.75em;top:.425rem;right:.5rem;line-height:1;text-transform:uppercase;color:#999}
.listingblock:hover code[data-lang]::before{display:block}
.listingblock.terminal pre .command::before{content:attr(data-prompt);padding-right:.5em;color:#999}
.listingblock.terminal pre .command:not([data-prompt])::before{content:"$"}
table.pyhltable{border-collapse:separate;border:0;margin-bottom:0;background:none}
table.pyhltable td{vertical-align:top;padding-top:0;padding-bottom:0;line-height:1.45}
table.pyhltable td.code{padding-left:.75em;padding-right:0}
pre.pygments .lineno,table.pyhltable td:not(.code){color:#999;padding-left:0;padding-right:.5em;border-right:1px solid #dddddf}
pre.pygments .lineno{display:inline-block;margin-right:.25em}
table.pyhltable .linenodiv{background:none!important;padding-right:0!important}
.quoteblock{margin:0 1em 1.25em 1.5em;display:table}
.quoteblock>.title{margin-left:-1.5em;margin-bottom:.75em}
.quoteblock blockquote,.quoteblock p{color:rgba(0,0,0,.85);font-size:1.15rem;line-height:1.75;word-spacing:.1em;letter-spacing:0;font-style:italic;text-align:justify}
.quoteblock blockquote{margin:0;padding:0;border:0}
.quoteblock blockquote::before{content:"\201c";float:left;font-size:2.75em;font-weight:bold;line-height:.6em;margin-left:-.6em;color:#7a2518;text-shadow:0 1px 2px rgba(0,0,0,.1)}
.quoteblock blockquote>.paragraph:last-child p{margin-bottom:0}
.quoteblock .attribution{margin-top:.75em;margin-right:.5ex;text-align:right}
.verseblock{margin:0 1em 1.25em}
.verseblock pre{font-family:"Open Sans","DejaVu Sans",sans;font-size:1.15rem;color:rgba(0,0,0,.85);font-weight:300;text-rendering:optimizeLegibility}
.verseblock pre strong{font-weight:400}
.verseblock .attribution{margin-top:1.25rem;margin-left:.5ex}
.quoteblock .attribution,.verseblock .attribution{font-size:.9375em;line-height:1.45;font-style:italic}
.quoteblock .attribution br,.verseblock .attribution br{display:none}
.quoteblock .attribution cite,.verseblock .attribution cite{display:block;letter-spacing:-.025em;color:rgba(0,0,0,.6)}
.quoteblock.abstract blockquote::before,.quoteblock.excerpt blockquote::before,.quoteblock .quoteblock blockquote::before{display:none}
.quoteblock.abstract blockquote,.quoteblock.abstract p,.quoteblock.excerpt blockquote,.quoteblock.excerpt p,.quoteblock .quoteblock blockquote,.quoteblock .quoteblock p{line-height:1.6;word-spacing:0}
.quoteblock.abstract{margin:0 1em 1.25em;display:block}
.quoteblock.abstract>.title{margin:0 0 .375em;font-size:1.15em;text-align:center}
.quoteblock.excerpt,.quoteblock .quoteblock{margin:0 0 1.25em;padding:0 0 .25em 1em;border-left:.25em solid #dddddf}
.quoteblock.excerpt blockquote,.quoteblock.excerpt p,.quoteblock .quoteblock blockquote,.quoteblock .quoteblock p{color:inherit;font-size:1.0625rem}
.quoteblock.excerpt .attribution,.quoteblock .quoteblock .attribution{color:inherit;text-align:left;margin-right:0}
table.tableblock{max-width:100%;border-collapse:separate}
p.tableblock:last-child{margin-bottom:0}
td.tableblock>.content{margin-bottom:-1.25em}
table.tableblock,th.tableblock,td.tableblock{border:0 solid #dedede}
table.grid-all>thead>tr>.tableblock,table.grid-all>tbody>tr>.tableblock{border-width:0 1px 1px 0}
table.grid-all>tfoot>tr>.tableblock{border-width:1px 1px 0 0}
table.grid-cols>*>tr>.tableblock{border-width:0 1px 0 0}
table.grid-rows>thead>tr>.tableblock,table.grid-rows>tbody>tr>.tableblock{border-width:0 0 1px}
table.grid-rows>tfoot>tr>.tableblock{border-width:1px 0 0}
table.grid-all>*>tr>.tableblock:last-child,table.grid-cols>*>tr>.tableblock:last-child{border-right-width:0}
table.grid-all>tbody>tr:last-child>.tableblock,table.grid-all>thead:last-child>tr>.tableblock,table.grid-rows>tbody>tr:last-child>.tableblock,table.grid-rows>thead:last-child>tr>.tableblock{border-bottom-width:0}
table.frame-all{border-width:1px}
table.frame-sides{border-width:0 1px}
table.frame-topbot,table.frame-ends{border-width:1px 0}
table.stripes-all tr,table.stripes-odd tr:nth-of-type(odd){background:#f8f8f7}
table.stripes-none tr,table.stripes-odd tr:nth-of-type(even){background:none}
th.halign-left,td.halign-left{text-align:left}
th.halign-right,td.halign-right{text-align:right}
th.halign-center,td.halign-center{text-align:center}
th.valign-top,td.valign-top{vertical-align:top}
th.valign-bottom,td.valign-bottom{vertical-align:bottom}
th.valign-middle,td.valign-middle{vertical-align:middle}
table thead th,table tfoot th{font-weight:bold}
tbody tr th{display:table-cell;line-height:1.6;background:#f7f8f7}
tbody tr th,tbody tr th p,tfoot tr th,tfoot tr th p{color:rgba(0,0,0,.8);font-weight:bold}
p.tableblock>code:only-child{background:none;padding:0}
p.tableblock{font-size:1em}
td>div.verse{white-space:pre}
ol{margin-left:1.75em}
ul li ol{margin-left:1.5em}
dl dd{margin-left:1.125em}
dl dd:last-child,dl dd:last-child>:last-child{margin-bottom:0}
ol>li p,ul>li p,ul dd,ol dd,.olist .olist,.ulist .ulist,.ulist .olist,.olist .ulist{margin-bottom:.625em}
ul.checklist,ul.none,ol.none,ul.no-bullet,ol.no-bullet,ol.unnumbered,ul.unstyled,ol.unstyled{list-style-type:none}
ul.no-bullet,ol.no-bullet,ol.unnumbered{margin-left:.625em}
ul.unstyled,ol.unstyled{margin-left:0}
ul.checklist{margin-left:.625em}
ul.checklist li>p:first-child>.fa-square-o:first-child,ul.checklist li>p:first-child>.fa-check-square-o:first-child{width:1.25em;font-size:.8em;position:relative;bottom:.125em}
ul.checklist li>p:first-child>input[type="checkbox"]:first-child{margin-right:.25em}
ul.inline{display:-ms-flexbox;display:-webkit-box;display:flex;-ms-flex-flow:row wrap;-webkit-flex-flow:row wrap;flex-flow:row wrap;list-style:none;margin:0 0 .625em -1.25em}
ul.inline>li{margin-left:1.25em}
.unstyled dl dt{font-weight:400;font-style:normal}
ol.arabic{list-style-type:decimal}
ol.decimal{list-style-type:decimal-leading-zero}
ol.loweralpha{list-style-type:lower-alpha}
ol.upperalpha{list-style-type:upper-alpha}
ol.lowerroman{list-style-type:lower-roman}
ol.upperroman{list-style-type:upper-roman}
ol.lowergreek{list-style-type:lower-greek}
.hdlist>table,.colist>table{border:0;background:none}
.hdlist>table>tbody>tr,.colist>table>tbody>tr{background:none}
td.hdlist1,td.hdlist2{vertical-align:top;padding:0 .625em}
td.hdlist1{font-weight:bold;padding-bottom:1.25em}
.literalblock+.colist,.listingblock+.colist{margin-top:-.5em}
.colist td:not([class]):first-child{padding:.4em .75em 0;line-height:1;vertical-align:top}
.colist td:not([class]):first-child img{max-width:none}
.colist td:not([class]):last-child{padding:.25em 0}
.thumb,.th{line-height:0;display:inline-block;border:solid 4px #fff;-webkit-box-shadow:0 0 0 1px #ddd;box-shadow:0 0 0 1px #ddd}
.imageblock.left{margin:.25em .625em 1.25em 0}
.imageblock.right{margin:.25em 0 1.25em .625em}
.imageblock>.title{margin-bottom:0}
.imageblock.thumb,.imageblock.th{border-width:6px}
.imageblock.thumb>.title,.imageblock.th>.title{padding:0 .125em}
.image.left,.image.right{margin-top:.25em;margin-bottom:.25em;display:inline-block;line-height:0}
.image.left{margin-right:.625em}
.image.right{margin-left:.625em}
a.image{text-decoration:none;display:inline-block}
a.image object{pointer-events:none}
sup.footnote,sup.footnoteref{font-size:.875em;position:static;vertical-align:super}
sup.footnote a,sup.footnoteref a{text-decoration:none}
sup.footnote a:active,sup.footnoteref a:active{text-decoration:underline}
#footnotes{padding-top:.75em;padding-bottom:.75em;margin-bottom:.625em}
#footnotes hr{width:20%;min-width:6.25em;margin:-.25em 0 .75em;border-width:1px 0 0}
#footnotes .footnote{padding:0 .375em 0 .225em;line-height:1.3334;font-size:.875em;margin-left:1.2em;margin-bottom:.2em}
#footnotes .footnote a:first-of-type{font-weight:bold;text-decoration:none;margin-left:-1.05em}
#footnotes .footnote:last-of-type{margin-bottom:0}
#content #footnotes{margin-top:-.625em;margin-bottom:0;padding:.75em 0}
.gist .file-data>table{border:0;background:#fff;width:100%;margin-bottom:0}
.gist .file-data>table td.line-data{width:99%}
div.unbreakable{page-break-inside:avoid}
.big{font-size:larger}
.small{font-size:smaller}
.underline{text-decoration:underline}
.overline{text-decoration:overline}
.line-through{text-decoration:line-through}
.aqua{color:#00bfbf}
.aqua-background{background-color:#00fafa}
.black{color:#000}
.black-background{background-color:#000}
.blue{color:#0000bf}
.blue-background{background-color:#0000fa}
.fuchsia{color:#bf00bf}
.fuchsia-background{background-color:#fa00fa}
.gray{color:#606060}
.gray-background{background-color:#7d7d7d}
.green{color:#006000}
.green-background{background-color:#007d00}
.lime{color:#00bf00}
.lime-background{background-color:#00fa00}
.maroon{color:#600000}
.maroon-background{background-color:#7d0000}
.navy{color:#000060}
.navy-background{background-color:#00007d}
.olive{color:#606000}
.olive-background{background-color:#7d7d00}
.purple{color:#600060}
.purple-background{background-color:#7d007d}
.red{color:#bf0000}
.red-background{background-color:#fa0000}
.silver{color:#909090}
.silver-background{background-color:#bcbcbc}
.teal{color:#006060}
.teal-background{background-color:#007d7d}
.white{color:#bfbfbf}
.white-background{background-color:#fafafa}
.yellow{color:#bfbf00}
.yellow-background{background-color:#fafa00}
span.icon>.fa{cursor:default}
a span.icon>.fa{cursor:inherit}
.admonitionblock td.icon [class^="fa icon-"]{font-size:2.5em;text-shadow:1px 1px 2px rgba(0,0,0,.5);cursor:default}
.admonitionblock td.icon .icon-note::before{content:"\f05a";color:#19407c}
.admonitionblock td.icon .icon-tip::before{content:"\f0eb";text-shadow:1px 1px 2px rgba(155,155,0,.8);color:#111}
.admonitionblock td.icon .icon-warning::before{content:"\f071";color:#bf6900}
.admonitionblock td.icon .icon-caution::before{content:"\f06d";color:#bf3400}
.admonitionblock td.icon .icon-important::before{content:"\f06a";color:#bf0000}
.conum[data-value]{display:inline-block;color:#fff!important;background-color:rgba(0,0,0,.8);-webkit-border-radius:100px;border-radius:100px;text-align:center;font-size:.75em;width:1.67em;height:1.67em;line-height:1.67em;font-family:"Open Sans","DejaVu Sans",sans-serif;font-style:normal;font-weight:bold}
.conum[data-value] *{color:#fff!important}
.conum[data-value]+b{display:none}
.conum[data-value]::after{content:attr(data-value)}
pre .conum[data-value]{position:relative;top:-.125em}
b.conum *{color:inherit!important}
.conum:not([data-value]):empty{display:none}
dt,th.tableblock,td.content,div.footnote{text-rendering:optimizeLegibility}
h1,h2,p,td.content,span.alt{letter-spacing:-.01em}
p strong,td.content strong,div.footnote strong{letter-spacing:-.005em}
p,blockquote,dt,td.content,span.alt{font-size:1.0625rem}
p{margin-bottom:1.25rem}
.sidebarblock p,.sidebarblock dt,.sidebarblock td.content,p.tableblock{font-size:1em}
.exampleblock>.content{background-color:#fffef7;border-color:#e0e0dc;-webkit-box-shadow:0 1px 4px #e0e0dc;box-shadow:0 1px 4px #e0e0dc}
.print-only{display:none!important}
@page{margin:1.25cm .75cm}
@media print{*{-webkit-box-shadow:none!important;box-shadow:none!important;text-shadow:none!important}
html{font-size:80%}
a{color:inherit!important;text-decoration:underline!important}
a.bare,a[href^="#"],a[href^="mailto:"]{text-decoration:none!important}
a[href^="http:"]:not(.bare)::after,a[href^="https:"]:not(.bare)::after{content:"(" attr(href) ")";display:inline-block;font-size:.875em;padding-left:.25em}
abbr[title]::after{content:" (" attr(title) ")"}
pre,blockquote,tr,img,object,svg{page-break-inside:avoid}
thead{display:table-header-group}
svg{max-width:100%}
p,blockquote,dt,td.content{font-size:1em;orphans:3;widows:3}
h2,h3,#toctitle,.sidebarblock>.content>.title{page-break-after:avoid}
#toc,.sidebarblock,.exampleblock>.content{background:none!important}
#toc{border-bottom:1px solid #dddddf!important;padding-bottom:0!important}
body.book #header{text-align:center}
body.book #header>h1:first-child{border:0!important;margin:2.5em 0 1em}
body.book #header .details{border:0!important;display:block;padding:0!important}
body.book #header .details span:first-child{margin-left:0!important}
body.book #header .details br{display:block}
body.book #header .details br+span::before{content:none!important}
body.book #toc{border:0!important;text-align:left!important;padding:0!important;margin:0!important}
body.book #toc,body.book #preamble,body.book h1.sect0,body.book .sect1>h2{page-break-before:always}
.listingblock code[data-lang]::before{display:block}
#footer{padding:0 .9375em}
.hide-on-print{display:none!important}
.print-only{display:block!important}
.hide-for-print{display:none!important}
.show-for-print{display:inherit!important}}
@media print,amzn-kf8{#header>h1:first-child{margin-top:1.25rem}
.sect1{padding:0!important}
.sect1+.sect1{border:0}
#footer{background:none}
#footer-text{color:rgba(0,0,0,.6);font-size:.9em}}
@media amzn-kf8{#header,#content,#footnotes,#footer{padding:0}}
</style>
</head>
<body class="article">
<div id="header">
<h1><NAME> - Project Portfolio</h1>
</div>
<div id="content">
<div class="sect1">
<h2 id="_project_wishbook">PROJECT: WishBook</h2>
<div class="sectionbody">
<hr>
</div>
</div>
<div class="sect1">
<h2 id="_overview">Overview</h2>
<div class="sectionbody">
<div class="paragraph">
<p>WishBook (WB) is a piggy bank in a digital app form, made for people who need a user-friendly solution for keeping track
of their disposable income. Not only does WB keep records of savings, it also allows users to set items as goals
for users to work towards, serving as a way to cultivate the habit of saving. WB is made for people who have material
wants in life, but are uncertain of purchasing said wants, by helping them to reserve disposable income so that they
know they can spend on their wants. The user interacts with it by using a Command Line Interface (CLI), and it has a
GUI created with JavaFX. It is written in Java, and has about 15 kLoC.</p>
</div>
<div class="paragraph">
<p>WishBook is an application adapted from addressbook-level4, the original application developed by the se-edu team.</p>
</div>
<div class="paragraph">
<p>The source code of addressbook-level4 can be found <a href="https://github.com/nus-cs2103-AY1819S1/addressbook-level4">here</a>.</p>
</div>
<div class="paragraph">
<p>This project portfolio consists of the contributions I have made to WishBook over the semester.<br></p>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_summary_of_contributions">Summary of contributions</h2>
<div class="sectionbody">
<div class="ulist">
<ul>
<li>
<p><strong>Major enhancement</strong>: Added <strong>the ability to save towards a wish or remove an amount from a wish.</strong></p>
<div class="ulist">
<ul>
<li>
<p>What it does: Allows the user to save a specified amount of money to a particular wish or remove a specified amount
from it. A wish will be fulfilled when the user saves up to 100% of a wish&#8217;s price and excess amount cannot be saved
towards it any longer.</p>
</li>
<li>
<p>Justification: This feature is a crucial feature in the WishBook as it cannot fulfill its primary requirement
without it.The fact that two actions, namely saving and removing can be done with a single command means that there are
less commands to remember.</p>
</li>
<li>
<p>Highlights: This enhancement touches multiple components and required thorough consideration of alternative design.
Hence it is naturally my most time consuming contribution. Significant effort was also expended in modifying the tests
to test this new feature.</p>
</li>
</ul>
</div>
</li>
<li>
<p><strong>Minor enhancement</strong>: Added <strong>the ability to find wishes by name, tag and/or remark with specified level
of match with keywords.</strong></p>
<div class="ulist">
<ul>
<li>
<p>Justification: The flexible design of this command allows user to specify varying number of arguments to the wish
which gives the user multiple ways to find a wish or a group of wishes for ease of access.</p>
</li>
<li>
<p>Highlights: It required an in-depth analysis of design alternatives. The implementation too was challenging as
it required rewriting and adapting the code written for an existing command. Significant effort was also expended in
modifying the tests to test this new feature.</p>
</li>
</ul>
</div>
</li>
<li>
<p><strong>Minor enhancement</strong>: Converted email attribute in a wish to date attribute which stores the due date of a wish. The
date cannot be earlier or equal to the current date.</p>
</li>
<li>
<p><strong>Minor enhancement</strong>: Made the wish list to be permanently sorted by due date regardless of the commands applied
on them.</p>
<div class="ulist">
<ul>
<li>
<p>Highlights: Required modification of test data which led to modification of a significant number of tests.</p>
</li>
</ul>
</div>
</li>
<li>
<p><strong>Minor enhancement</strong>: Made the wishes be identified only by a newly added attribute, UUID. This allows
for addition of wishes with the same attribute values, something which was not allowed previously. UUID of a wish also
cannot be changed upon addition of a wish.</p>
</li>
<li>
<p><strong>Minor enhancement</strong>: Added a remark command.</p>
<div class="ulist">
<ul>
<li>
<p>Credits: Code was adapted from <a href="https://github.com/se-edu/addressbook-level4/pull/599" class="bare">https://github.com/se-edu/addressbook-level4/pull/599</a>.</p>
</li>
</ul>
</div>
</li>
<li>
<p><strong>Code contributed</strong>: [<a href="https://nus-cs2103-ay1819s1.github.io/cs2103-dashboard/#=undefined&amp;search=dioptrique&amp;sort=displayName&amp;since=2018-09-12&amp;until=&amp;timeframe=day&amp;reverse=false&amp;repoSort=true">Functional code</a>]</p>
</li>
<li>
<p><strong>Other contributions</strong>:</p>
<div class="ulist">
<ul>
<li>
<p>Project management:</p>
<div class="ulist">
<ul>
<li>
<p>Managed release <code>v1.1</code> on GitHub</p>
</li>
</ul>
</div>
</li>
<li>
<p>Documentation:</p>
<div class="ulist">
<ul>
<li>
<p>Did structural changes to User Guide: <a href="https://github.com/CS2103-AY1819S1-T16-1/main/pull/67">#67</a>,
<a href="https://github.com/CS2103-AY1819S1-T16-1/main/pull/136">#136</a></p>
</li>
</ul>
</div>
</li>
<li>
<p>Community:</p>
<div class="ulist">
<ul>
<li>
<p>Reported bugs and suggestions for group members (examples:  <a href="https://github.com/CS2103-AY1819S1-T16-1/main/issues/71">#71</a>,
<a href="https://github.com/CS2103-AY1819S1-T16-1/main/issues/20">#20</a>,
<a href="https://github.com/CS2103-AY1819S1-T16-1/main/issues/127">#127</a>,
<a href="https://github.com/CS2103-AY1819S1-T16-1/main/issues/130">#130</a>,
<a href="https://github.com/CS2103-AY1819S1-T16-1/main/issues/77">#5</a>,
 <a href="https://github.com/CS2103-AY1819S1-T16-1/main/issues/70">#6</a>,
 <a href="https://github.com/CS2103-AY1819S1-T16-1/main/issues/85">#7</a>,
 <a href="https://github.com/CS2103-AY1819S1-T16-1/main/issues/72">#8</a>)</p>
</li>
<li>
<p>Reported bugs and suggestions for other teams (examples: <a href="https://github.com/CS2103-AY1819S1-F11-3/main/issues/197">#197</a>,
<a href="https://github.com/CS2103-AY1819S1-F11-3/main/issues/194">#194</a>,
<a href="https://github.com/CS2103-AY1819S1-F11-3/main/issues/189">#189</a>,
<a href="https://github.com/CS2103-AY1819S1-F11-3/main/issues/186">#186</a>)</p>
</li>
</ul>
</div>
</li>
</ul>
</div>
</li>
</ul>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_contributions_to_the_user_guide">Contributions to the User Guide</h2>
<div class="sectionbody">
<table class="tableblock frame-all grid-all stretch">
<colgroup>
<col style="width: 100%;">
</colgroup>
<tbody>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><em>Given below are sections I contributed to the User Guide. They showcase my ability to write documentation
targeting end-users.</em></p></td>
</tr>
</tbody>
</table>
<div class="sect2">
<h3 id="_locate_your_wish_with_speed_and_precision_find">Locate your wish with speed and precision: <code>find</code></h3>
<div class="paragraph">
<p>Find wishes which match the given search keywords.<br>
Format: <code>find [-e] [n/NAME_KEYWORD]&#8230;&#8203; [t/TAG_KEYWORD]&#8230;&#8203; [r/REMARK_KEYWORD]&#8230;&#8203;</code></p>
</div>
<div class="sidebarblock">
<div class="content">
<div class="ulist">
<ul>
<li>
<p>At least one keyword must be provided.</p>
</li>
<li>
<p>Searching multiple keywords of the <strong>same prefix</strong> will return wishes whose
attribute corresponding to the prefix contain <strong>any</strong> one of the keywords.</p>
</li>
<li>
<p>Searching with keywords of <strong>different prefixes</strong> will return only wishes that
match will <strong>all</strong> the keywords of the different prefixes.</p>
</li>
<li>
<p>Using the exact match flag, <code>-e</code> returns wishes whose corresponding attributes contain <strong>all</strong>
the keywords.</p>
</li>
<li>
<p>The search is case insensitive. e.g. watch will match Watch.</p>
</li>
</ul>
</div>
</div>
</div>
<div class="paragraph">
<p>Examples:</p>
</div>
<div class="ulist">
<ul>
<li>
<p><code>find n/wat</code><br>
Returns any wish whose name contains the <em>wat</em>.</p>
</li>
<li>
<p><code>find n/wat n/balloon n/appl</code><br>
Returns wishes whose names which contain at least any one of <em>wat</em>, <em>balloon</em> or <em>appl</em>.</p>
</li>
<li>
<p><code>find -e n/wat n/balloon n/appl</code><br>
Returns only wishes whose names that contain all of <em>wat</em>, <em>balloon</em> and <em>appl</em>.</p>
</li>
<li>
<p><code>find n/watch t/important</code><br>
Returns any wish whose name contains <em>watch</em>, and whose tags contains <em>broke wishes</em>.</p>
</li>
<li>
<p><code>find n/wat n/balloon t/important</code><br>
Returns any wish whose name contains <em>wat</em> or <em>balloon</em>, and whose tags contains
<em>important</em>.</p>
</li>
</ul>
</div>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_contributions_to_the_developer_guide">Contributions to the Developer Guide</h2>
<div class="sectionbody">
<table class="tableblock frame-all grid-all stretch">
<colgroup>
<col style="width: 100%;">
</colgroup>
<tbody>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><em>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.</em></p></td>
</tr>
</tbody>
</table>
<div class="sect2">
<h3 id="_model">Model</h3>
<div class="sect3">
<h4 id="_wish_model">Wish Model</h4>
<div class="paragraph">
<p>A wish is uniquely identified by its Universal Unique Identifier (UUID) which is generated randomly only once for a
particular wish, upon its creation through the <code>AddCommand</code>. A wish stores the following primary attributes:</p>
</div>
<div class="ulist">
<ul>
<li>
<p>Name</p>
</li>
<li>
<p>Price</p>
</li>
<li>
<p>Date</p>
</li>
<li>
<p>Saved Amount</p>
</li>
<li>
<p>Url</p>
</li>
<li>
<p>Remark</p>
</li>
<li>
<p>Tags</p>
</li>
<li>
<p>UUID</p>
</li>
</ul>
</div>
<div class="admonitionblock note">
<table>
<tr>
<td class="icon">
<div class="title">Note</div>
</td>
<td class="content">
It is impossible for the user to create a duplicate wish as it is impossible to modify a wish&#8217;s UUID.
</td>
</tr>
</table>
</div>
</div>
<div class="sect3">
<h4 id="_wish_priority">Wish Priority</h4>
<div class="paragraph">
<p>A wish needs to be prioritised in a specific order such that the wishes with the highest priority will be visible
on the top of the list. In WishBook, the priority is determined primarily by the due date of the wish which is
stored in every wish&#8217;s <code>Date</code> attribute. Ties are broken by <code>Name</code>. Further ties are broken by <code>UUID</code> as it is possible
for the <code>Date</code> and <code>Name</code> of two wishes to be identical.</p>
</div>
<div class="paragraph">
<p>The sorting of the displayed results is done by the <code>filteredSortedWishes</code> list. The sorting order is specified by
<code>WishComparator</code>.</p>
</div>
</div>
<div class="sect3">
<h4 id="_design_considerations">Design Considerations</h4>
<div class="sect4">
<h5 id="_aspect_uniqueness_of_a_wish">Aspect: Uniqueness of a Wish</h5>
<div class="ulist">
<ul>
<li>
<p><strong>Alternative 1(current choice):</strong> Identify a <code>Wish</code> by a randomly generated UUID.</p>
<div class="ulist">
<ul>
<li>
<p>Pros: Extremely low probability of collision.</p>
</li>
<li>
<p>Pros: No extra maintenance required upon generation as every <code>Wish</code> is unique.</p>
</li>
<li>
<p>Cons: UUID does not map to any real world entity and it is used strictly for identification.</p>
</li>
<li>
<p>Cons: It is more difficult to system test the <code>AddCommand</code> with the current group of methods for system tests as
UUID is randomly generated each time.</p>
</li>
</ul>
</div>
</li>
<li>
<p><strong>Alternative 2:</strong> Identify a wish by <code>Name</code>, <code>Price</code>, <code>Date</code>, <code>Url</code>, <code>Tags</code>. Wishes with identical values for
these attributes will be represented by a single <code>WishCard</code>. The <code>WishCard</code> will be augmented with a <code>Multiplicity</code> to
indicate the number of identical wishes.</p>
<div class="ulist">
<ul>
<li>
<p>Pros: WishBook will be more compact and every attribute stored in a <code>Wish</code> maps to a real entity.</p>
</li>
<li>
<p>Cons: Additional attribute <code>Multiplicity</code> may have to be frequently edited as it is another
attribute that is affected by multiple commands.</p>
</li>
</ul>
</div>
</li>
<li>
<p><strong>Alternative 3:</strong> Identify a wish by a new attribute <code>CreatedTime</code>, which is derived from the system time
when the wish is created.</p>
<div class="ulist">
<ul>
<li>
<p>Pros: The attribute maps to a real entity. It can be an additional information presented to the user about a wish.</p>
</li>
<li>
<p>Cons: There might be collisions in <code>CreatedTime</code> if the the system time is incorrect.</p>
</li>
</ul>
</div>
</li>
</ul>
</div>
</div>
</div>
</div>
<div class="sect2">
<h3 id="_find_wish_feature">Find Wish Feature</h3>
<div class="sect3">
<h4 id="_current_implementation">Current Implementation</h4>
<div class="paragraph">
<p>The find mechanism is supported by <code>FindCommandParser</code>. It implements <code>Parser</code> that implements the following operation:</p>
</div>
<div class="ulist">
<ul>
<li>
<p><code>FindCommandParser#parse()</code>&#8201;&#8212;&#8201;Checks the arguments for empty strings and throws a <code>ParseException</code> if empty string is
found. It then splits the arguments using <code>ArgumentTokenizer#tokenize()</code> and returns an <code>ArgumentMultimap</code>. Keywords of
the same prefix are then grouped using <code>ArgumentMultimap#getAllValues()</code>.</p>
</li>
</ul>
</div>
<div class="paragraph">
<p>The find mechanism is also facilitated by <code>FindCommand</code>. It extends <code>Command</code> and implements the
following operation:</p>
</div>
<div class="ulist">
<ul>
<li>
<p><code>FindCommand#execute()</code>&#8201;&#8212;&#8201;Executes the command by updating the current <code>FilteredSortedWishList</code> with the
<code>WishContainsKeywordPredicate</code>.</p>
</li>
</ul>
</div>
<div class="paragraph">
<p>The predicate <code>WishContainsKeywordsPredicate</code>, takes in three lists of the keywords for the following attributes:</p>
</div>
<div class="ulist">
<ul>
<li>
<p>Name</p>
</li>
<li>
<p>Tags</p>
</li>
<li>
<p>Remark</p>
</li>
</ul>
</div>
<div class="paragraph">
<p>and also the <code>isExactMatch</code> argument. The result of the predicate is determined by checking
whether a <code>Wish</code> contains the given keywords at their corresponding attributes. The match threshold
is dictated by the value of <code>isExactMatch</code>.</p>
</div>
</div>
<div class="sect3">
<h4 id="_example">Example</h4>
<div class="paragraph">
<p>Given below is an example usage scenario and how the Find mechanism behaves at each step.</p>
</div>
<div class="paragraph">
<p>Step 1. The user launches the application for the first time.</p>
</div>
<div class="paragraph">
<p>Step 2. The user executes <code>find n/wat n/apple t/impor</code> command to get all wishes whose name contains the
keywords 'iphone' or 'tablet'.</p>
</div>
<div class="paragraph">
<p>Step 3. The <code>FindCommandParser#parse()</code> is called and the <code>WishContainsKeywordPredicate</code> is constructed with
the arguments of the find command.</p>
</div>
<div class="paragraph">
<p>Step 4. <code>FindCommand#execute()</code> is then called.</p>
</div>
<div class="paragraph">
<p>Step 5. The entire list of wishes is filtered by the predicate <code>WishContainsKeywordsPredicate</code>.</p>
</div>
<div class="paragraph">
<p>Step 6. The filtered list of wishes is returned to the GUI.</p>
</div>
<div class="imageblock">
<div class="content">
<img src="../images/FindCommandSequenceDiagram.png" alt="FindCommandSequenceDiagram" width="800">
</div>
</div>
</div>
<div class="sect3">
<h4 id="_design_considerations_2">Design Considerations</h4>
<div class="sect4">
<h5 id="_aspect_argument_format">Aspect: Argument format</h5>
<div class="ulist">
<ul>
<li>
<p><strong>Alternative 1 (Current choice):</strong> Require the user to prepend every keyword argument with the appropriate
Wish attribute prefix.</p>
<div class="ulist">
<ul>
<li>
<p>Pros: Easier to implement as it easier to match keyword against a Wish if the attribute to match against is known.</p>
</li>
<li>
<p>Pros: User has more control over the results returned.</p>
</li>
<li>
<p>Cons: User is required to type slightly more.</p>
</li>
</ul>
</div>
</li>
<li>
<p><strong>Alternative 2:</strong> No prefixes are required in the arguments. Keywords can match with any one of the
following chosen wish attributes: <code>Name</code>, <code>Tags</code> or <code>Remark</code>.</p>
<div class="ulist">
<ul>
<li>
<p>Pros: Less typing required from user.</p>
</li>
<li>
<p>Cons: Command might be slightly slower as every keyword has to be checked against all chosen attributes of the
wish.</p>
</li>
<li>
<p>Cons: User has less control over the results returned.</p>
</li>
</ul>
</div>
</li>
</ul>
</div>
</div>
</div>
<div class="sect3">
<h4 id="_aspect_default_threshold_for_match_without_the_exact_match_flag">Aspect: Default threshold for match without the exact match flag</h4>
<div class="ulist">
<ul>
<li>
<p><strong>Alternative 1 (Current choice):</strong> Keywords appended to different prefixes are grouped with a logical AND and
keywords appended to the same prefixes are grouped with a logical OR when being matched against a <code>Wish</code>.</p>
<div class="ulist">
<ul>
<li>
<p>Pros: A more intuitive way to find wishes.</p>
</li>
<li>
<p>Cons: Can be restrictive in some situations.</p>
</li>
</ul>
</div>
</li>
<li>
<p><strong>Alternative 2:</strong> Keywords appended to different prefixes are grouped with a logical OR and
keywords appended to the same prefixes are grouped with a logical OR when being matched against a <code>Wish</code>.</p>
<div class="ulist">
<ul>
<li>
<p>Pros: Search results will be more inclusive.</p>
</li>
<li>
<p>Cons: Very slim chance for such a use case.</p>
</li>
</ul>
</div>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="footer">
<div id="footer-text">
Last updated 2018-11-12 21:55:45 +0800
</div>
</div>
</body>
</html>
<file_sep>/src/main/java/seedu/address/logic/commands/AddCommand.java
package seedu.address.logic.commands;

import static java.util.Objects.requireNonNull;
import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE;
import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME;
import static seedu.address.logic.parser.CliSyntax.PREFIX_PRICE;
import static seedu.address.logic.parser.CliSyntax.PREFIX_REMARK;
import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG;
import static seedu.address.logic.parser.CliSyntax.PREFIX_URL;

import seedu.address.logic.CommandHistory;
import seedu.address.logic.commands.exceptions.CommandException;
import seedu.address.model.Model;
import seedu.address.model.wish.Wish;

/**
 * Adds a wish to the address book.
 */
public class AddCommand extends Command {

    public static final String COMMAND_WORD = "add";
    public static final String COMMAND_ALIAS = "a";

    public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a wish to the address book. "
            + "Parameters: "
            + PREFIX_NAME + "NAME "
            + PREFIX_PRICE + "PRICE "
            + PREFIX_DATE + "DATE "
            + "[" + PREFIX_URL + "URL] "
            + "[" + PREFIX_REMARK + "REMARK] "
            + "[" + PREFIX_TAG + "TAG]...\n"
            + "Example: " + COMMAND_WORD + " "
            + PREFIX_NAME + "iPad 10 "
            + PREFIX_PRICE + "6080.50 "
            + PREFIX_DATE + "20/11/2021 "
            + PREFIX_URL + "https://www.amazon.com/gp/product/B07D998212 "
            + PREFIX_REMARK + "For dad. "
            + PREFIX_TAG + "electronics "
            + PREFIX_TAG + "personal ";

    public static final String MESSAGE_SUCCESS = "New wish added: %1$s";
    public static final String MESSAGE_DUPLICATE_WISH = "This wish already exists in the address book";

    private final Wish toAdd;

    /**
     * Creates an AddCommand to add the specified {@code Wish}
     */
    public AddCommand(Wish wish) {
        requireNonNull(wish);
        toAdd = wish;
    }

    @Override
    public CommandResult execute(Model model, CommandHistory history) throws CommandException {
        requireNonNull(model);

        if (model.hasWish(toAdd)) {
            throw new CommandException(MESSAGE_DUPLICATE_WISH);
        }

        model.addWish(toAdd);
        model.commitWishBook();
        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));
    }

    /**
     * A weaker notion of equality between {@code AddCommand}.
     */
    public boolean isSameAs(AddCommand other) {
        return other == this
                || toAdd.getName().equals(other.toAdd.getName())
                && toAdd.getPrice().equals(other.toAdd.getPrice())
                && toAdd.getDate().equals(other.toAdd.getDate());
    }
}
<file_sep>/src/main/java/seedu/address/ui/WishDetailSavingHistory.java
package seedu.address.ui;

import java.util.ArrayList;
import java.util.Collections;
import java.util.ListIterator;
import java.util.logging.Logger;

import com.google.common.eventbus.Subscribe;

import javafx.collections.FXCollections;
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.commons.events.model.WishBookChangedEvent;
import seedu.address.commons.events.ui.WishDataUpdatedEvent;
import seedu.address.commons.events.ui.WishPanelSelectionChangedEvent;
import seedu.address.model.WishTransaction;
import seedu.address.model.wish.Wish;

/**
 * Panel containing the detail of wish.
 */
public class WishDetailSavingHistory extends UiPart<Region> {
    private static final String FXML = "WishDetailSavingHistory.fxml";
    private final Logger logger = LogsCenter.getLogger(getClass());

    private WishTransaction wishTransaction;
    private ArrayList savingHistoryList = new ArrayList();
    private String id;

    @FXML
    private ListView<String> savingHistoryListView;

    public WishDetailSavingHistory(WishTransaction wishTransaction) {
        super(FXML);

        this.wishTransaction = wishTransaction;

        registerAsAnEventHandler(this);
    }

    private void setConnections(ObservableList<String> savingHistoryList) {
        savingHistoryListView.setItems(savingHistoryList);
        savingHistoryListView.setCellFactory(listView -> new WishDetailSavingHistory.SavingHistoryListViewCell());
    }

    /**
     * Load the page that shows the detail of wish.
     */
    private void loadWishDetails(Wish wish) {
        this.savingHistoryList.clear();
        this.id = wish.getId().toString();

        ListIterator<Wish> entry = wishTransaction.getWishMap().get(wish.getId()).listIterator(1);

        while (entry.hasNext()) {
            Wish prevWish = wishTransaction.getWishMap().get(wish.getId()).get(entry.previousIndex());
            double prevAmount = prevWish.getSavedAmount().value;
            double nextAmount = entry.next().getSavedAmount().value;
            double diff = nextAmount - prevAmount;

            if (diff > 0) {
                savingHistoryList.add("Saved $" + String.format("%.2f", diff));
            } else if (diff < 0) {
                savingHistoryList.add("Deducted $" + String.format("%.2f", Math.abs(diff)));
            }
        }

        Collections.reverse(savingHistoryList);

        ObservableList<String> oSavingHistoryList = FXCollections.observableArrayList(savingHistoryList);
        setConnections(oSavingHistoryList);
    }

    @Subscribe
    private void handleWishPanelSelectionChangedEvent(WishPanelSelectionChangedEvent event) {
        logger.info(LogsCenter.getEventHandlingLogMessage(event));
        loadWishDetails(event.getNewSelection());
    }

    @Subscribe
    private void handleWishDataUpdatedEvent(WishDataUpdatedEvent event) {
        logger.info(LogsCenter.getEventHandlingLogMessage(event,
                "wish data updated by " + WishDetailSavingHistory.class.getSimpleName()));
        if (this.id.equals(event.getNewData().getId().toString())) {
            loadWishDetails(event.getNewData());
        }
    }

    @Subscribe
    private void handleWishBookChangedEvent(WishBookChangedEvent event) {
        logger.info(LogsCenter.getEventHandlingLogMessage(event,
                "handled by " + WishDetailSavingHistory.class.getSimpleName()));
        this.wishTransaction = event.wishTransaction;
    }

    /**
     * Custom {@code ListCell} that displays the graphics of a {@code String} using a {@code WishSavingHistoryCell}.
     */
    class SavingHistoryListViewCell extends ListCell<String> {
        @Override
        protected void updateItem(String savingHistory, boolean empty) {
            super.updateItem(savingHistory, empty);

            if (empty || savingHistory == null) {
                setGraphic(null);
                setText(null);
            } else {
                setGraphic(new SavingHistoryCell(savingHistory).getRoot());
            }
        }
    }
}
<file_sep>/src/main/java/seedu/address/model/wish/Wish.java
package seedu.address.model.wish;

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 java.util.UUID;
import java.util.stream.Collectors;

import seedu.address.commons.core.amount.Amount;
import seedu.address.model.tag.Tag;

/**
 * Represents a Wish in the address book.
 * Guarantees: details are present and not null, field values are validated, immutable.
 */
public class Wish {

    // Identity fields
    private final UUID id;

    // Data fields
    private final Name name;
    private final Price price;
    private final Date date;
    private final Url url;
    private final Remark remark;
    private final Set<Tag> tags = new HashSet<>();
    private final SavedAmount savedAmount;
    private final boolean fulfilled;
    private final boolean expired;


    /**
     * Constructs an object for an already created {@code Wish}.
     * Every field must be present and not null.
     */
    public Wish(Name name, Price price, Date date, Url url, SavedAmount savedAmount,
                Remark remark, Set<Tag> tags, UUID id) {
        requireAllNonNull(name, price, date, url, tags);

        fulfilled = isSavedAmountGreaterThanOrEqualToPrice(savedAmount, price);
        expired = isCurrDateGreaterThanOrEqualToDate(date);

        this.name = name;
        this.price = price;
        this.date = date;
        this.url = url;
        this.tags.addAll(tags);
        this.remark = remark;
        this.savedAmount = savedAmount;

        this.id = id;
    }

    public Wish(Wish wish) {
        this(new Name(wish.name), new Price(wish.price), new Date(wish.date), new Url(wish.url),
                new SavedAmount(wish.savedAmount), new Remark(wish.remark), wish.copyTags(), wish.id);
    }

    /**
     * Every field must be present and not null.
     */
    private Wish(Name name, Price price, Date date, Url url, SavedAmount savedAmount, Remark remark, Set<Tag> tags) {
        requireAllNonNull(name, price, date, url, tags);

        fulfilled = isSavedAmountGreaterThanOrEqualToPrice(savedAmount, price);
        expired = isCurrDateGreaterThanOrEqualToDate(date);

        this.name = name;
        this.price = price;
        this.date = date;
        this.url = url;
        this.tags.addAll(tags);
        this.remark = remark;
        this.savedAmount = savedAmount;

        this.id = UUID.randomUUID();
    }

    /**
     * Performs a deep copy on each tag in {@code tags} and returns the copied set of tags.
     * @return the deep copied set of tags.
     */
    private Set<Tag> copyTags() {
        Set<Tag> copy = tags.stream().map(Tag::new).collect(Collectors.toSet());
        return copy;
    }

    /**
     * Returns a newly created {@code Wish} with a new id.
     */
    public static Wish createWish(Name name, Price price, Date date, Url url,
                                  SavedAmount savedAmount, Remark remark, Set<Tag> tags) {
        requireAllNonNull(name, price, date, url, tags);
        return new Wish(name, price, date, url, savedAmount, remark, tags);
    }

    /**
     * Returns a newly created {@code Wish} with an incremented savedAmount.
     * @param oldWish The wish to increment the saved amount on.
     * @param amountToIncrement The amount to increment by.
     */
    public static Wish createWishWithIncrementedSavedAmount(Wish oldWish, Amount amountToIncrement) {
        requireAllNonNull(oldWish, amountToIncrement);
        return new Wish(oldWish.getName(), oldWish.getPrice(), oldWish.getDate(), oldWish.getUrl(),
                oldWish.getSavedAmount().incrementSavedAmount(amountToIncrement), oldWish.getRemark(),
                oldWish.getTags(), oldWish.getId());
    }

    /**
     * Returns true if SaveAmount exceeds Price of wish.
     */
    private boolean isSavedAmountGreaterThanOrEqualToPrice(SavedAmount savedAmount, Price price) {
        return savedAmount.value >= price.value;
    }

    /*
     * Returns true if CurrDate exceeds Date of wish.
     */
    private boolean isCurrDateGreaterThanOrEqualToDate(Date date) {
        return new java.util.Date().after(date.getDateObject());
    }

    public Name getName() {
        return name;
    }

    public Price getPrice() {
        return price;
    }

    public Date getDate() {
        return date;
    }

    public SavedAmount getSavedAmount() {
        return savedAmount;
    }

    public Url getUrl() {
        return url;
    }

    public Remark getRemark() {
        return remark;
    }

    public boolean isFulfilled() {
        return fulfilled;
    }

    public boolean isExpired() {
        return expired;
    }

    public UUID getId() {
        return id;
    }

    /**
     * Returns an immutable tag set, which throws {@code UnsupportedOperationException}
     * if modification is attempted.
     */
    public Set<Tag> getTags() {
        return Collections.unmodifiableSet(tags);
    }

    /**
     * Returns true only if two wishes have the same id.
     */
    public boolean isSameWish(Wish otherWish) {
        if (otherWish == this) {
            return true;
        }

        return otherWish != null
                && otherWish.getId().equals(getId());
    }

    /**
     * Returns the progress for {@code wish}, ranges from 0.0 to 1.0.
     */
    public Double getProgress() {
        return getSavedAmount().value / getPrice().value;
    }

    /**
     * Returns the {@code savedAmount} - {@code price} for {@code wish}.
     */
    public Amount getSavedAmountToPriceDifference() {
        return Amount.add(new Amount(savedAmount.toString()), new Amount("-" + price.toString()));
    }

    /**
     * Returns true if both wishes have the same identity and data fields.
     * This defines a stronger notion of equality between two wishes.
     */
    @Override
    public boolean equals(Object other) {
        if (other == this) {
            return true;
        }

        if (!(other instanceof Wish)) {
            return false;
        }

        Wish otherWish = (Wish) other;
        return otherWish.getName().equals(getName())
                && otherWish.getPrice().equals(getPrice())
                && otherWish.getDate().equals(getDate())
                && otherWish.getUrl().equals(getUrl())
                && otherWish.getTags().equals(getTags())
                && otherWish.getId().equals(getId());
    }

    @Override
    public int hashCode() {
        // use this method for custom fields hashing instead of implementing your own
        return Objects.hash(name, price, date, url, tags);
    }

    @Override
    public String toString() {
        final StringBuilder builder = new StringBuilder();
        builder.append(getName())
                .append(" Price: ")
                .append(getPrice())
                .append(" Date: ")
                .append(getDate())
                .append(" Url: ")
                .append(getUrl())
                .append(" Remark: ")
                .append(getRemark())
                .append(" Tags: ");
        getTags().forEach(builder::append);
        return builder.toString();
    }

}
<file_sep>/src/test/java/seedu/address/model/wish/PriceTest.java
package seedu.address.model.wish;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import static seedu.address.logic.commands.CommandTestUtil.VALID_PRICE_AMY;

import org.junit.Test;

import seedu.address.testutil.Assert;

public class PriceTest {

    @Test
    public void constructor_null_throwsNullPointerException() {
        Assert.assertThrows(NullPointerException.class, () -> new Price((String) null));
        Assert.assertThrows(NullPointerException.class, () -> new Price((Price) null));
    }

    @Test
    public void constructor_invalidPrice_throwsIllegalArgumentException() {
        String invalidPrice = "";
        Assert.assertThrows(IllegalArgumentException.class, () -> new Price(invalidPrice));
    }

    @Test
    public void copyConstructor_success() {
        Price price = new Price("93028");
        Price copy = new Price(price);
        assertEquals(price, copy);
    }

    @Test
    public void isValidPrice() {
        // null price value
        Assert.assertThrows(NullPointerException.class, () -> Price.isValidPrice(null));

        // invalid prices
        assertFalse(Price.isValidPrice("")); // empty string
        assertFalse(Price.isValidPrice(" ")); // spaces only
        assertFalse(Price.isValidPrice("91..1")); // double dot
        assertFalse(Price.isValidPrice("price")); // non-numeric
        assertFalse(Price.isValidPrice("1.0e321")); // exp
        assertFalse(Price.isValidPrice("9312 1534")); // spaces within digits
        assertFalse(Price.isValidPrice("2.")); // no digits after decimal point

        // valid prices
        assertTrue(Price.isValidPrice("93121534")); // no decimal digit
        assertTrue(Price.isValidPrice("1.0")); // one decimal digit
        assertTrue(Price.isValidPrice("1.02")); // two decimal digits
    }

    @Test
    public void toStringTest() {
        Price price = new Price(VALID_PRICE_AMY);
        assertTrue(price.toString().equals(VALID_PRICE_AMY));
    }

    @Test
    public void hashCodeTest() {
        assertTrue(new Price(VALID_PRICE_AMY).hashCode()
                == new Price(VALID_PRICE_AMY).hashCode());
    }
}
<file_sep>/src/main/java/seedu/address/model/WishBook.java
package seedu.address.model;

import static java.util.Objects.requireNonNull;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javafx.collections.ObservableList;
import seedu.address.commons.core.amount.Amount;
import seedu.address.model.tag.Tag;
import seedu.address.model.wish.SavedAmount;
import seedu.address.model.wish.UniqueWishList;
import seedu.address.model.wish.Wish;
import seedu.address.model.wish.exceptions.DuplicateWishException;


/**
 * Wraps all data at the wish book level
 * Duplicates are not allowed (by .isSameWish comparison)
 */
public class WishBook implements ReadOnlyWishBook {

    private final UniqueWishList wishes;
    private SavedAmount unusedFunds;

    /*
     * 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.
     */
    {
        wishes = new UniqueWishList();
        unusedFunds = new SavedAmount("" + 0.0);
    }

    public WishBook() {}

    /**
     * Creates an WishBook using the Wishes in the {@code toBeCopied}
     */
    public WishBook(ReadOnlyWishBook toBeCopied) {
        this();
        resetData(toBeCopied);
    }

    //// list overwrite operations

    /**
     * Replaces the contents of the wish list with {@code wishes}.
     * {@code wishes} must not contain duplicate wishes.
     */
    public void setWishes(List<Wish> wishes) {
        this.wishes.setWishes(wishes);
    }

    /**
     * Resets the existing data of this {@code WishBook} with {@code newData}.
     */
    public void resetData(ReadOnlyWishBook newData) {
        requireNonNull(newData);

        setWishes(newData.getWishList());
        setUnusedFunds(newData.getUnusedFunds());
    }

    /**
     * Update the value of unused funds.
     * @param change The amount to increment/decrease by.
     */
    public void updateUnusedFunds(Amount change) {
        this.unusedFunds = this.unusedFunds.incrementSavedAmount(change);
    }

    /**
     * Sets the value of the unused funds.
     * @param amount The amount to set.
     */
    public void setUnusedFunds(SavedAmount amount) {
        this.unusedFunds = amount;
    }

    @Override
    public SavedAmount getUnusedFunds() {
        return this.unusedFunds;
    }

    //// wish-level operations

    /**
     * Returns true if a wish with the same identity as {@code wish} exists in the wish book.
     */
    public boolean hasWish(Wish wish) {
        requireNonNull(wish);
        return wishes.contains(wish);
    }

    /**
     * Adds a wish to the wish book.
     * The wish must not already exist in the wish book.
     */
    public void addWish(Wish p) {
        wishes.add(p);
    }

    /**
     * Replaces the given wish {@code target} in the list with {@code editedWish}.
     * {@code target} must exist in the wish book.
     * The wish identity of {@code editedWish} must not be the same as another existing wish in the wish book.
     */
    public void updateWish(Wish target, Wish editedWish) {
        requireNonNull(editedWish);

        wishes.setWish(target, editedWish);
    }

    /**
     * Removes {@code key} from this {@code WishBook}.
     * {@code key} must exist in the wish book.
     */
    public void removeWish(Wish key) {
        if (!key.isFulfilled()) {
            updateUnusedFunds(key.getSavedAmount().getAmount());
        }
        wishes.remove(key);
    }

    /**
     * Removes {@code tag} from all {@code wish}es in this {@code WishBook}.
     * @throws DuplicateWishException if there's a duplicate {@code Wish} in this {@code WishBook}.
     */
    public void removeTagFromAll(Tag tag) throws DuplicateWishException {
        ArrayList<Wish> modifiedWishes = new ArrayList<>();

        for (Wish wish : wishes.asUnmodifiableObservableList()) {
            Set<Tag> modifiedTags = new HashSet(wish.getTags());
            modifiedTags.removeIf(t -> t == tag);

            Wish modifiedWish = new Wish(wish.getName(),
                    wish.getPrice(),
                    wish.getDate(),
                    wish.getUrl(),
                    wish.getSavedAmount(),
                    wish.getRemark(), modifiedTags, wish.getId());

            modifiedWishes.add(modifiedWish);
        }
        wishes.setWishes(modifiedWishes);
    }


    //// util methods

    @Override
    public String toString() {
        return wishes.asUnmodifiableObservableList().size() + " wishes";
        // TODO: refine later
    }

    @Override
    public ObservableList<Wish> getWishList() {
        return wishes.asUnmodifiableObservableList();
    }

    @Override
    public boolean equals(Object other) {
        return other == this // short circuit if same object
                || (other instanceof WishBook // instanceof handles nulls
                && wishes.equals(((WishBook) other).wishes)
                && unusedFunds.equals(((WishBook) other).unusedFunds));
    }

    @Override
    public int hashCode() {
        return wishes.hashCode();
    }
}
<file_sep>/docs/team/dioptrique.adoc
= <NAME> - Project Portfolio
:site-section: AboutUs
:imagesDir: ../images
:stylesDir: ../stylesheets

== PROJECT: WishBook

---

== Overview

WishBook (WB) is a piggy bank in a digital app form, made for people who need a user-friendly solution for keeping track
of their disposable income. Not only does WB keep records of savings, it also allows users to set items as goals
for users to work towards, serving as a way to cultivate the habit of saving. WB is made for people who have material
wants in life, but are uncertain of purchasing said wants, by helping them to reserve disposable income so that they
know they can spend on their wants. The user interacts with it by using a Command Line Interface (CLI), and it has a
GUI created with JavaFX. It is written in Java, and has about 15 kLoC.

WishBook is an application adapted from addressbook-level4, the original application developed by the se-edu team.

The source code of addressbook-level4 can be found https://github.com/nus-cs2103-AY1819S1/addressbook-level4[here].

This project portfolio consists of the contributions I have made to WishBook over the semester. +

== Summary of contributions

* *Major enhancement*: Added *the ability to save towards a wish or remove an amount from a wish.*
** What it does: Allows the user to save a specified amount of money to a particular wish or remove a specified amount
from it. A wish will be fulfilled when the user saves up to 100% of a wish's price and excess amount cannot be saved
towards it any longer.
** Justification: This feature is a crucial feature in the WishBook as it cannot fulfill its primary requirement
without it.The fact that two actions, namely saving and removing can be done with a single command means that there are
less commands to remember.
** Highlights: This enhancement touches multiple components and required thorough consideration of alternative design.
Hence it is naturally my most time consuming contribution. Significant effort was also expended in modifying the tests
to test this new feature.

* *Minor enhancement*: Added *the ability to find wishes by name, tag and/or remark with specified level
of match with keywords.*
** Justification: The flexible design of this command allows user to specify varying number of arguments to the wish
which gives the user multiple ways to find a wish or a group of wishes for ease of access.
** Highlights: It required an in-depth analysis of design alternatives. The implementation too was challenging as
it required rewriting and adapting the code written for an existing command. Significant effort was also expended in
modifying the tests to test this new feature.

* *Minor enhancement*: Converted email attribute in a wish to date attribute which stores the due date of a wish. The
date cannot be earlier or equal to the current date.

* *Minor enhancement*: Made the wish list to be permanently sorted by due date regardless of the commands applied
on them.
** Highlights: Required modification of test data which led to modification of a significant number of tests.

* *Minor enhancement*: Made the wishes be identified only by a newly added attribute, UUID. This allows
for addition of wishes with the same attribute values, something which was not allowed previously. UUID of a wish also
cannot be changed upon addition of a wish.

* *Minor enhancement*: Added a remark command.
** Credits: Code was adapted from https://github.com/se-edu/addressbook-level4/pull/599.

* *Code contributed*: [https://nus-cs2103-ay1819s1.github.io/cs2103-dashboard/#=undefined&search=dioptrique&sort=displayName&since=2018-09-12&until=&timeframe=day&reverse=false&repoSort=true[Project Code Dashboard link]]

* *Other contributions*:

** Project management:
*** Managed release `v1.1` on GitHub
** Documentation:
*** Did structural changes to User Guide: https://github.com/CS2103-AY1819S1-T16-1/main/pull/67[#67],
https://github.com/CS2103-AY1819S1-T16-1/main/pull/136[#136]
** Community:
*** Reported bugs and suggestions for group members (examples:  https://github.com/CS2103-AY1819S1-T16-1/main/issues/71[#71],
                                                               https://github.com/CS2103-AY1819S1-T16-1/main/issues/20[#20],
                                                               https://github.com/CS2103-AY1819S1-T16-1/main/issues/127[#127],
                                                               https://github.com/CS2103-AY1819S1-T16-1/main/issues/130[#130],
                                                               https://github.com/CS2103-AY1819S1-T16-1/main/issues/77[#5],
                                                                https://github.com/CS2103-AY1819S1-T16-1/main/issues/70[#6],
                                                                https://github.com/CS2103-AY1819S1-T16-1/main/issues/85[#7],
                                                                https://github.com/CS2103-AY1819S1-T16-1/main/issues/72[#8])
*** Reported bugs and suggestions for other teams (examples: https://github.com/CS2103-AY1819S1-F11-3/main/issues/197[#197],
                                                             https://github.com/CS2103-AY1819S1-F11-3/main/issues/194[#194],
                                                             https://github.com/CS2103-AY1819S1-F11-3/main/issues/189[#189],
                                                             https://github.com/CS2103-AY1819S1-F11-3/main/issues/186[#186])

== 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=find]

== 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=implementationmodel]

include::../DeveloperGuide.adoc[tag=find]

<file_sep>/docs/AboutUs.adoc
= About Us
:site-section: AboutUs
:relfileprefix: team/
:imagesDir: images
:stylesDir: stylesheets

WishBook was developed by the https://github.com/orgs/CS2103-AY1819S1-T16-1/teams/developers[GaveSoals] team. +

We are a team based in the http://www.comp.nus.edu.sg[School of Computing, National University of Singapore].

The original application (addressbook-level4) was developed by the https://se-edu.github.io/docs/Team.html[se-edu] team. +
The source code can be found https://github.com/nus-cs2103-AY1819S1/addressbook-level4[here].

== Project Team

=== <NAME>, Dominic
image::bannified.png[width="150", align="left"]
{empty} [https://github.com/bannified[github]] [<<bannified#, portfolio>>]

Role: Team Lead +
Responsibilities: Scheduling and Tasking, In charge of Model

'''

=== <NAME>
image::leongshengmin.png[width="150", align="left"]
{empty}[http://github.com/leongshengmin[github]] [<<leongshengmin#, portfolio>>]

Role: Developer +
Responsibilities: Code Quality, In charge of Storage

'''

=== <NAME>
image::dioptrique.png[width="150", align="left"]
{empty}[http://github.com/dioptrique[github]] [<<dioptrique#, portfolio>>]

Role: Developer +
Responsibilities: Testing, Integration

'''

=== <NAME>
image::lionelat.png[width="150", align="left"]
{empty}[http://github.com/lionelat[github]] [<<lionelat#, portfolio>>]

Role: Developer +
Responsibilities: Documentation, In charge of Logic, Deliverables and Deadlines

'''

=== <NAME>
image::jiholim.png[width="150", align="left"]
{empty}[https://mobbin.design/[homepage]] [http://github.com/jiholim[github]] [<<jiholim_ppp#, portfolio>>]

Role: Developer +
Responsibilities: UI, In charge of UI component

'''
<file_sep>/src/test/java/seedu/address/testutil/EditWishDescriptorBuilder.java
package seedu.address.testutil;

import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import seedu.address.logic.commands.EditCommand.EditWishDescriptor;
import seedu.address.model.tag.Tag;
import seedu.address.model.wish.Date;
import seedu.address.model.wish.Name;
import seedu.address.model.wish.Price;
import seedu.address.model.wish.Url;
import seedu.address.model.wish.Wish;

/**
 * A utility class to help with building EditWishDescriptor objects.
 */
public class EditWishDescriptorBuilder {

    private EditWishDescriptor descriptor;

    public EditWishDescriptorBuilder() {
        descriptor = new EditWishDescriptor();
    }

    public EditWishDescriptorBuilder(EditWishDescriptor descriptor) {
        this.descriptor = new EditWishDescriptor(descriptor);
    }

    /**
     * Returns an {@code EditWishDescriptor} with fields containing {@code wish}'s details
     */
    public EditWishDescriptorBuilder(Wish wish) {
        descriptor = new EditWishDescriptor();
        descriptor.setName(wish.getName());
        descriptor.setPrice(wish.getPrice());
        descriptor.setDate(wish.getDate());
        descriptor.setUrl(wish.getUrl());
        descriptor.setTags(wish.getTags());
    }

    /**
     * Sets the {@code Name} of the {@code EditWishDescriptor} that we are building.
     */
    public EditWishDescriptorBuilder withName(String name) {
        descriptor.setName(new Name(name));
        return this;
    }

    /**
     * Sets the {@code Price} of the {@code EditWishDescriptor} that we are building.
     */
    public EditWishDescriptorBuilder withPrice(String price) {
        descriptor.setPrice(new Price(price));
        return this;
    }

    /**
     * Sets the {@code Date} of the {@code EditWishDescriptor} that we are building.
     * @param date
     */
    public EditWishDescriptorBuilder withDate(String date) {
        descriptor.setDate(new Date(date));
        return this;
    }

    /**
     * Sets the {@code Url} of the {@code EditWishDescriptor} that we are building.
     */
    public EditWishDescriptorBuilder withAddress(String url) {
        descriptor.setUrl(new Url(url));
        return this;
    }

    /**
     * Parses the {@code tags} into a {@code Set<Tag>} and set it to the {@code EditWishDescriptor}
     * that we are building.
     */
    public EditWishDescriptorBuilder withTags(String... tags) {
        Set<Tag> tagSet = Stream.of(tags).map(Tag::new).collect(Collectors.toSet());
        descriptor.setTags(tagSet);
        return this;
    }

    public EditWishDescriptor build() {
        return descriptor;
    }
}
<file_sep>/src/main/java/seedu/address/storage/XmlWishTransactionStorage.java
package seedu.address.storage;

import static java.util.Objects.requireNonNull;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
import java.util.logging.Logger;

import seedu.address.commons.core.LogsCenter;
import seedu.address.commons.exceptions.DataConversionException;
import seedu.address.commons.exceptions.IllegalValueException;
import seedu.address.commons.util.FileUtil;
import seedu.address.model.WishTransaction;

/**
 * A class to access WishTransaction data stored as an xml file on the hard disk.
 */
public class XmlWishTransactionStorage implements WishTransactionStorage {

    private static final Logger logger = LogsCenter.getLogger(XmlWishBookStorage.class);

    private Path filePath;

    public XmlWishTransactionStorage(Path filePath) {
        this.filePath = filePath;
    }

    @Override
    public Path getWishTransactionFilePath() {
        return filePath;
    }

    @Override
    public Optional<WishTransaction> readWishTransaction() throws DataConversionException, IOException {
        return readWishTransaction(filePath);
    }

    @Override
    public Optional<WishTransaction> readWishTransaction(Path filePath) throws DataConversionException, IOException {
        requireNonNull(filePath);

        logger.info("Reading wishTransaction in XmlWishTransactionStorage.");
        if (!Files.exists(filePath)) {
            logger.info("WishTransaction file " + filePath + " not found");
            return Optional.empty();
        }

        XmlWishTransactions xmlWishTransactions = XmlFileStorage.loadWishTransactionDataFromFile(filePath);
        try {
            logger.info("Converting WishTransaction from XML to model type in XmlWishTransactionStorage.");
            WishTransaction wishTransaction = xmlWishTransactions.toModelType();
            return Optional.of(wishTransaction);
        } catch (IllegalValueException ive) {
            logger.info("Illegal values found in WishTransaction file at " + filePath + ": " + ive.getMessage());
            throw new DataConversionException(ive);
        }
    }

    @Override
    public void saveWishTransaction(WishTransaction wishTransaction) throws IOException {
        saveWishTransaction(wishTransaction, filePath);
    }

    @Override
    public void saveWishTransaction(WishTransaction wishTransaction, Path filePath) throws IOException {
        requireNonNull(wishTransaction);
        requireNonNull(filePath);

        FileUtil.createIfMissing(filePath);
        XmlFileStorage.saveDataToFile(filePath, new XmlWishTransactions(wishTransaction));
    }
}
<file_sep>/src/main/java/seedu/address/model/util/WishComparator.java
package seedu.address.model.util;

import java.util.Comparator;

import seedu.address.model.wish.Date;
import seedu.address.model.wish.Wish;

/**
 * Compares Wishes using Wish fields.
 */
public class WishComparator implements Comparator<Wish> {

    @Override
    public int compare(Wish o1, Wish o2) {
        assert(Date.isValidDate(o1.getDate().date));
        assert(Date.isValidDate(o1.getDate().date));
        java.util.Date date1 = o1.getDate().getDateObject();
        java.util.Date date2 = o2.getDate().getDateObject();
        int nameComparison = o1.getName().fullName.compareTo(o2.getName().fullName);

        if (date1.after(date2)) {
            return 1;
        } else if (date1.before(date2)) {
            return -1;
        } else if (nameComparison == 1) {
            return 1;
        } else if (nameComparison == -1) {
            return -1;
        } else {
            return o1.getId().compareTo(o2.getId());
        }
    }


    @Override
    public boolean equals(Object obj) {
        return false;
    }
}
<file_sep>/src/test/java/seedu/address/ui/WishCardTest.java
package seedu.address.ui;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static seedu.address.ui.testutil.GuiTestAssert.assertCardDisplaysWish;

import org.junit.Test;

import guitests.guihandles.WishCardHandle;
import seedu.address.model.wish.Wish;
import seedu.address.testutil.WishBuilder;

public class WishCardTest extends GuiUnitTest {

    @Test
    public void display() {
        // no tags
        Wish wishWithNoTags = new WishBuilder().withTags(new String[0]).build();
        WishCard wishCard = new WishCard(wishWithNoTags, 1);
        uiPartRule.setUiPart(wishCard);
        assertCardDisplay(wishCard, wishWithNoTags, 1);

        // with tags
        Wish wishWithTags = new WishBuilder().build();
        wishCard = new WishCard(wishWithTags, 2);
        uiPartRule.setUiPart(wishCard);
        assertCardDisplay(wishCard, wishWithTags, 2);
    }

    @Test
    public void equals() {
        Wish wish = new WishBuilder().build();
        WishCard wishCard = new WishCard(wish, 0);

        // same wish, same index -> returns true
        WishCard copy = new WishCard(wish, 0);
        assertTrue(wishCard.equals(copy));

        // same object -> returns true
        assertTrue(wishCard.equals(wishCard));

        // null -> returns false
        assertFalse(wishCard.equals(null));

        // different types -> returns false
        assertFalse(wishCard.equals(0));

        // different wish, same index -> returns false
        Wish differentWish = new WishBuilder().withName("differentName").build();
        assertFalse(wishCard.equals(new WishCard(differentWish, 0)));

        // same wish, different index -> returns false
        assertFalse(wishCard.equals(new WishCard(wish, 1)));
    }

    /**
     * Asserts that {@code wishCard} displays the details of {@code expectedWish} correctly and matches
     * {@code expectedId}.
     */
    private void assertCardDisplay(WishCard wishCard, Wish expectedWish, int expectedId) {
        guiRobot.pauseForHuman();

        WishCardHandle wishCardHandle = new WishCardHandle(wishCard.getRoot());

        // verify id is displayed correctly
        //assertEquals(Integer.toString(expectedId) + ". ", wishCardHandle.getId());

        // verify wish details are displayed correctly
        assertCardDisplaysWish(expectedWish, wishCardHandle);
    }
}
<file_sep>/src/main/java/seedu/address/storage/XmlAdaptedWishWrapper.java
package seedu.address.storage;

import static java.util.Objects.requireNonNull;

import java.util.Iterator;
import java.util.LinkedList;

import javax.xml.bind.annotation.XmlElementWrapper;

/**
 * This Xml adapted class acts as a wrapper for a collection of {@code XmlAdaptedWish}es.
 */
public class XmlAdaptedWishWrapper {
    @XmlElementWrapper
    private LinkedList<XmlAdaptedWish> xmlAdaptedWishes;

    public XmlAdaptedWishWrapper() {
        this.xmlAdaptedWishes = new LinkedList<>();
    }

    public XmlAdaptedWishWrapper(LinkedList<XmlAdaptedWish> xmlAdaptedWishes) {
        requireNonNull(xmlAdaptedWishes);
        this.xmlAdaptedWishes = xmlAdaptedWishes;
    }

    public LinkedList<XmlAdaptedWish> getXmlAdaptedWishes() {
        return xmlAdaptedWishes;
    }

    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof XmlAdaptedWishWrapper) || !(hasSameNumberOfWishes((XmlAdaptedWishWrapper) obj))) {
            return false;
        }

        if (((XmlAdaptedWishWrapper) obj).xmlAdaptedWishes.isEmpty() && xmlAdaptedWishes.isEmpty()) {
            return true;
        }

        Iterator<XmlAdaptedWish> otherIterator = ((XmlAdaptedWishWrapper) obj).getXmlAdaptedWishes().listIterator();
        Iterator<XmlAdaptedWish> thisIterator = xmlAdaptedWishes.listIterator();

        while (otherIterator.hasNext()) {
            if (!thisIterator.next().equals(otherIterator.next())) {
                return false;
            }
        }
        return true;
    }

    /**
     * Checks if this has the same number of {@code XmlAdaptedWish} objects as the other {@code XmlAdaptedWishWrapper}.
     */
    private boolean hasSameNumberOfWishes(XmlAdaptedWishWrapper other) {
        return xmlAdaptedWishes.size() == other.getXmlAdaptedWishes().size();
    }
}
<file_sep>/src/test/java/seedu/address/logic/parser/FindCommandParserTest.java
package seedu.address.logic.parser;

import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure;
import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess;

import java.util.Arrays;

import org.junit.Test;

import seedu.address.logic.commands.FindCommand;
import seedu.address.model.wish.WishContainsKeywordsPredicate;

public class FindCommandParserTest {

    private FindCommandParser parser = new FindCommandParser();

    @Test
    public void parse_emptyArg_throwsParseException() {
        assertParseFailure(parser, "     ",
                String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE));
    }

    @Test
    public void parse_noPrefix_throwsParseException() {
        assertParseFailure(parser, "apple",
                String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE));
        assertParseFailure(parser, "-e",
                String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE));
    }

    @Test
    public void parse_invalidPrefix_throwsParseException() {
        assertParseFailure(parser, "p/22.30",
                String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE));
    }

    @Test
    public void parse_validArgs_returnsFindCommand() {
        /* isExactMatch set to false */

        // no leading and trailing whitespaces
        FindCommand expectedFindCommand =
                new FindCommand(new WishContainsKeywordsPredicate(Arrays.asList("Water", "Balloon"),
                        Arrays.asList("family", "important"), Arrays.asList("Visa", "must do"), false));
        assertParseSuccess(parser, " n/Water n/Balloon t/family t/important r/Visa r/must do", expectedFindCommand);

        // multiple whitespaces between keywords
        assertParseSuccess(parser, " n/Water \n n/Balloon          "
                + "t/family \n t/important   \n r/Visa      r/must do", expectedFindCommand);

        // No tags keywords
        expectedFindCommand =
                new FindCommand(new WishContainsKeywordsPredicate(Arrays.asList("Water", "Balloon"),
                        Arrays.asList(), Arrays.asList("Visa", "must do"), false));
        assertParseSuccess(parser, " n/Water n/Balloon r/Visa r/must do", expectedFindCommand);

        // No remark keywords
        expectedFindCommand =
                new FindCommand(new WishContainsKeywordsPredicate(Arrays.asList("Water", "Balloon"),
                        Arrays.asList("family", "important"), Arrays.asList(), false));
        assertParseSuccess(parser, " n/Water n/Balloon t/family t/important", expectedFindCommand);

        // No name keywords
        expectedFindCommand =
                new FindCommand(new WishContainsKeywordsPredicate(Arrays.asList(),
                        Arrays.asList("family", "important"), Arrays.asList("Visa", "must do"), false));
        assertParseSuccess(parser, " t/family t/important r/Visa r/must do", expectedFindCommand);

        // invalid preamble
        expectedFindCommand =
                new FindCommand(new WishContainsKeywordsPredicate(Arrays.asList("Water", "Balloon"),
                        Arrays.asList("family", "important"), Arrays.asList("Visa", "must do"), false));
        assertParseSuccess(parser, " dsfas;dkfjsd n/Water n/Balloon t/family t/important r/Visa r/must do",
                expectedFindCommand);

        // valid preamble
        expectedFindCommand =
                new FindCommand(new WishContainsKeywordsPredicate(Arrays.asList("Water", "Balloon"),
                        Arrays.asList("family", "important"), Arrays.asList("Visa", "must do"), true));
        assertParseSuccess(parser, " -e n/Water n/Balloon t/family t/important r/Visa r/must do", expectedFindCommand);

        // jumbled up order of prefixes
        expectedFindCommand =
                new FindCommand(new WishContainsKeywordsPredicate(Arrays.asList("Water", "Balloon"),
                        Arrays.asList("family", "important"), Arrays.asList("Visa", "must do"), false));
        assertParseSuccess(parser, " n/Water r/Visa t/family n/Balloon  t/important r/must do", expectedFindCommand);

    }

}
<file_sep>/README.adoc
= WishBook
ifdef::env-github,env-browser[:relfileprefix: docs/]

https://travis-ci.org/CS2103-AY1819S1-T16-1/main[image:https://travis-ci.org/se-edu/addressbook-level4.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://gitter.im/se-edu/Lobby[image:https://badges.gitter.im/se-edu/Lobby.svg[Gitter chat]]

ifdef::env-github[]
image::docs/images/Ui.png[width="600"]
endif::[]

ifndef::env-github[]
image::images/Ui.png[width="600"]
endif::[]

'''

_Are you saving for a house deposit, new car, holiday, household bill etc or putting money aside just in case?_

[%hardbreaks]

*Get WishBook and keep track of your savings and save more!*

'''

* WishBook is a desktop saving assistant application. It has a GUI but most of the user interactions happen using a CLI (Command Line Interface).
* It is a Java application built for people who want to set saving goals and keep track of their progress.

* The app boasts beautifully designed interface and has cool features like:
** Auto-calculates of the amount you need to save per month/week to reach your goal by the target date.
** Displays how much you have saved as a proportion of your goal.
** Shows how close you are to your target date.

== Site Map

* <<UserGuide#, User Guide>>
* <<DeveloperGuide#, Developer Guide>>
* <<AboutUs#, About Us>>
* <<ContactUs#, Contact Us>>

== Acknowledgements

* This application is based on https://github.com/nus-cs2103-AY1819S1/addressbook-level4[Address Book - Level 4] developed by the https://github.com/se-edu/[se-edu] team.
* Some parts of this sample application were inspired by the excellent http://code.makery.ch/library/javafx-8-tutorial/[Java FX tutorial] by
_<NAME>_.
* Libraries used: https://github.com/TestFX/TestFX[TextFX], https://bitbucket.org/controlsfx/controlsfx/[ControlsFX], https://github.com/FasterXML/jackson[Jackson], https://github.com/google/guava[Guava], https://github.com/junit-team/junit5[JUnit5]

== Licence : link:LICENSE[MIT]
<file_sep>/src/main/java/seedu/address/ui/WishDetailSavingAmount.java
package seedu.address.ui;

import java.util.logging.Logger;

import com.google.common.eventbus.Subscribe;

import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.layout.Region;
import seedu.address.commons.core.LogsCenter;
import seedu.address.commons.events.ui.WishDataUpdatedEvent;
import seedu.address.commons.events.ui.WishPanelSelectionChangedEvent;
import seedu.address.model.wish.Wish;

/**
 * Panel containing the detail of wish.
 */
public class WishDetailSavingAmount extends UiPart<Region> {

    private static final String FXML = "WishDetailSavingAmount.fxml";

    private final Logger logger = LogsCenter.getLogger(WishDetailSavingAmount.class);

    @FXML
    private Label price;

    @FXML
    private Label savedAmount;

    @FXML
    private Label progress;

    private String id;

    public WishDetailSavingAmount() {
        super(FXML);

        loadDefaultDetails();
        registerAsAnEventHandler(this);
    }

    /**
     * Load the default page.
     */
    public void loadDefaultDetails() {
        price.setText("");
        savedAmount.setText("");
    }

    /**
     * Load the page that shows the detail of wish.
     */
    private void loadWishDetails(Wish wish) {
        savedAmount.setText("$" + wish.getSavedAmount().toString());
        price.setText("/ $" + wish.getPrice().toString());
        progress.setText(getProgressInString(wish));
        this.id = wish.getId().toString();
    }

    /**
     * Returns the progress String (in percentage) for {@code wish}.
     */
    private String getProgressInString(Wish wish) {
        Double progress = wish.getProgress() * 100;
        return String.format("%d", progress.intValue()) + "%";
    }

    @Subscribe
    private void handleWishPanelSelectionChangedEvent(WishPanelSelectionChangedEvent event) {
        logger.info(LogsCenter.getEventHandlingLogMessage(event));
        loadWishDetails(event.getNewSelection());
    }

    @Subscribe
    private void handleWishDataUpdatedEvent(WishDataUpdatedEvent event) {
        logger.info(LogsCenter.getEventHandlingLogMessage(event));
        if (this.id.equals(event.getNewData().getId().toString())) {
            loadWishDetails(event.getNewData());
        }
    }
}
<file_sep>/src/test/java/seedu/address/logic/parser/SaveCommandParserTest.java
package seedu.address.logic.parser;

import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
import static seedu.address.logic.commands.CommandTestUtil.VALID_SAVED_AMOUNT_AMY;
import static seedu.address.logic.commands.SaveCommand.MESSAGE_USAGE;
import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure;
import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess;
import static seedu.address.logic.parser.SaveCommandParser.UNUSED_FUNDS_INDEX;
import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_WISH;

import org.junit.Test;

import seedu.address.commons.core.amount.Amount;
import seedu.address.commons.core.index.Index;
import seedu.address.logic.commands.SaveCommand;

public class SaveCommandParserTest {
    private SaveCommandParser saveCommandParser = new SaveCommandParser();
    private final Index targetIndex = INDEX_FIRST_WISH;
    private final Amount amount = new Amount(VALID_SAVED_AMOUNT_AMY);

    @Test
    public void parse_indexSpecified_success() {
        final String userInput = targetIndex.getOneBased() + " " + VALID_SAVED_AMOUNT_AMY;
        final SaveCommand expectedCommand = new SaveCommand(targetIndex, amount);
        assertParseSuccess(saveCommandParser, userInput, expectedCommand);

        final String userInputUnusedFundsIndex = UNUSED_FUNDS_INDEX + " " + VALID_SAVED_AMOUNT_AMY;
        final SaveCommand expectedCommandUnusedFunds = new SaveCommand(amount);
        assertParseSuccess(saveCommandParser, userInputUnusedFundsIndex, expectedCommandUnusedFunds);
    }

    @Test
    public void parse_missingCompulsoryField_failure() {
        final String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, MESSAGE_USAGE);

        final String userInputMissingIndex = VALID_SAVED_AMOUNT_AMY;
        assertParseFailure(saveCommandParser, userInputMissingIndex,
                expectedMessage);

        final String userInputMissingSavingAmount = targetIndex + " ";
        assertParseFailure(saveCommandParser, userInputMissingSavingAmount,
                expectedMessage);

        /*final String userInputMissingSavingAmountAndSavingPrefix = targetIndex + "";
        assertParseFailure(saveCommandParser, userInputMissingSavingAmountAndSavingPrefix,
                expectedMessage);*/
    }
}
