diff --git a/Collate-TUI.jar b/Collate-TUI.jar new file mode 100644 index 000000000000..2b9e032106dd Binary files /dev/null and b/Collate-TUI.jar differ diff --git a/README.adoc b/README.adoc index 03eff3a4d191..cf6966380831 100644 --- a/README.adoc +++ b/README.adoc @@ -1,10 +1,8 @@ -= Address Book (Level 4) += TravelBanker ifdef::env-github,env-browser[:relfileprefix: docs/] -https://travis-ci.org/se-edu/addressbook-level4[image:https://travis-ci.org/se-edu/addressbook-level4.svg?branch=master[Build Status]] -https://ci.appveyor.com/project/damithc/addressbook-level4[image:https://ci.appveyor.com/api/projects/status/3boko2x2vr5cc3w2?svg=true[Build status]] -https://coveralls.io/github/se-edu/addressbook-level4?branch=master[image:https://coveralls.io/repos/github/se-edu/addressbook-level4/badge.svg?branch=master[Coverage Status]] -https://www.codacy.com/app/damith/addressbook-level4?utm_source=github.com&utm_medium=referral&utm_content=se-edu/addressbook-level4&utm_campaign=Badge_Grade[image:https://api.codacy.com/project/badge/Grade/fc0b7775cf7f4fdeaf08776f3d8e364a[Codacy Badge]] +https://travis-ci.org/CS2103JAN2018-T11-B4/main[image:https://travis-ci.org/CS2103JAN2018-T11-B4/main.svg?branch=master[Build Status]] +https://coveralls.io/github/CS2103JAN2018-T11-B4/main?branch=master[image:https://coveralls.io/repos/github/CS2103JAN2018-T11-B4/main/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[] @@ -12,29 +10,47 @@ image::docs/images/Ui.png[width="600"] endif::[] ifndef::env-github[] -image::images/Ui.png[width="600"] +image::docs/images/Ui.png[width="600"] endif::[] -* This is a desktop Address Book application. It has a GUI but most of the user interactions happen using a CLI (Command Line Interface). -* It is a Java sample application intended for students learning Software Engineering while using Java as the main programming language. -* It is *written in OOP fashion*. It provides a *reasonably well-written* code example that is *significantly bigger* (around 6 KLoC)than what students usually write in beginner-level SE modules. -* What's different from https://github.com/se-edu/addressbook-level3[level 3]: -** A more sophisticated GUI that includes a list panel and an in-built Browser. -** More test cases, including automated GUI testing. -** Support for _Build Automation_ using Gradle and for _Continuous Integration_ using Travis CI. +== Introduction + +* This is a desktop java application that helps people, particularly travellers, to informally keep accounts with other people. +* Record how much money you owe someone, or someone owes to you, plus much more! +* Most of your user interactions are via command line, while there exists a GUI (Graphical User Interface). + +== Getting Started +For developers, please refer to the "DeveloperGuide.adoc" under "Section 1. Setting Up" +and ensure the steps are properly followed. + +For users, please refer to the "UserGuide.adoc" for instructions on how to +use the application. + +Below is a list of useful links to the relevant documents to get you started. == Site Map * <> * <> -* <> * <> * <> + +== Built With +- IntelliJ IDE - Software Development +- GitHub - Source Control + +== Authors (in Alphabetical Order) +- Artsiom Skliar - Developer +- Chen Chongsong - Developer +- Eric Lingfeng Zhou - Developer +- Prian Kuhanandan - Developer + == Acknowledgements * Some parts of this sample application were inspired by the excellent http://code.makery.ch/library/javafx-8-tutorial/[Java FX tutorial] by _Marco Jakob_. * Libraries used: https://github.com/TomasMikula/EasyBind[EasyBind], 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/junit4[JUnit4] +* Source code is based on the https://github.com/se-edu/addressbook-level4[AddressBook-Level4] project created by SE-EDU initiative. == Licence : link:LICENSE[MIT] diff --git a/UI.png b/UI.png new file mode 100644 index 000000000000..37bc556ac78c Binary files /dev/null and b/UI.png differ diff --git a/build.gradle b/build.gradle index 50cd2ae52efc..db21891f7b7d 100644 --- a/build.gradle +++ b/build.gradle @@ -24,6 +24,7 @@ targetCompatibility = JavaVersion.VERSION_1_8 repositories { mavenCentral() maven { url "https://oss.sonatype.org/content/repositories/snapshots/" } + maven { url "https://jitpack.io" } } checkstyle { @@ -46,6 +47,7 @@ dependencies { compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.7.0' compile group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: '2.7.4' compile group: 'com.google.guava', name: 'guava', version: '19.0' + compile 'com.github.Ritaja:java-exchange-rates:1.0' testCompile group: 'junit', name: 'junit', version: '4.12' testCompile group: 'org.testfx', name: 'testfx-core', version: testFxVersion diff --git a/collated/functional/Aritcho28-reused.md b/collated/functional/Aritcho28-reused.md new file mode 100644 index 000000000000..2112e56cb7a3 --- /dev/null +++ b/collated/functional/Aritcho28-reused.md @@ -0,0 +1,21 @@ +# Aritcho28-reused +###### /java/seedu/address/ui/PersonCard.java +``` java + /** + * Returns the color for a specific tag. + */ + private String getTagColor(String tagName) { + return TAG_COLOR[Math.abs(tagName.hashCode()) % TAG_COLOR.length]; + } + + /** + * Creates the tag colors for a person. + */ + private void initTags(Person person) { + person.getTags().forEach(tag -> { + Label tagLabel = new Label(tag.tagName); + tagLabel.getStyleClass().add(getTagColor(tag.tagName)); + tags.getChildren().add(tagLabel); + }); + } +``` diff --git a/collated/functional/Articho28.md b/collated/functional/Articho28.md new file mode 100644 index 000000000000..2db94a604b17 --- /dev/null +++ b/collated/functional/Articho28.md @@ -0,0 +1,376 @@ +# Articho28 +###### /java/seedu/address/commons/events/ui/ShowMapRequestEvent.java +``` java +/** + * An event requesting to view a map showing the nearest ATM. + */ +public class ShowMapRequestEvent extends BaseEvent { + + public String toString() { + return this.getClass().getSimpleName(); + } +} +``` +###### /java/seedu/address/logic/commands/BalanceCommand.java +``` java + +package seedu.address.logic.commands; + +import java.text.DecimalFormat; +import java.util.List; + +import seedu.address.model.person.Person; +/** + * Handles the balance command. + */ +public class BalanceCommand extends Command { + + public static final String COMMAND_WORD = "balance"; + public static final String COMMAND_SHORTCUT = "b"; + public static final String MESSAGE_SUCCESS = "Shown balance."; + public static final String MESSAGE_USAGE = COMMAND_WORD + "Displays your overall balance"; + private static double calculatedBalance; + private static DecimalFormat twoDecimalPlaces = new DecimalFormat("0.00"); + + public static DecimalFormat getFormatTwoDecimalPlaces() { + return twoDecimalPlaces; + } + + public static double getCalculatedBalance() { + return calculatedBalance; + } + + @Override + public CommandResult execute() { + + calculatedBalance = getBalanceFromTravelBanker(); + return new CommandResult(MESSAGE_SUCCESS + "\n" + "Your balance is " + + twoDecimalPlaces.format(calculatedBalance) + "."); + } + + public double getBalanceFromTravelBanker() { + double accumulator = 0.00; + + List lastShownList = model.getFilteredPersonList(); + + for (Person person : lastShownList) { + double currentPersonBalance = person.getMoney().balance; + accumulator = accumulator + currentPersonBalance; + } + + return accumulator; + } +} +``` +###### /java/seedu/address/logic/commands/MapCommand.java +``` java +/** + * Shows a map and searchs for the nearest ATM. + */ + +public class MapCommand extends Command { + + public static final String COMMAND_WORD = "map"; + public static final String COMMAND_SHORTCUT = "mp"; + public static final String MESSAGE_SUCCESS = "Map Shown"; + + @Override + public CommandResult execute() { + EventsCenter.getInstance().post(new ShowMapRequestEvent()); + return new CommandResult(MESSAGE_SUCCESS); + } + + +} +``` +###### /java/seedu/address/logic/commands/MinCommand.java +``` java +package seedu.address.logic.commands; + +import java.util.List; + +import seedu.address.commons.core.EventsCenter; +import seedu.address.commons.core.index.Index; + +import seedu.address.commons.events.ui.JumpToListRequestEvent; +import seedu.address.model.person.Person; +/** + * Finds the person to which you owe the most money + */ +public class MinCommand extends Command { + + public static final String COMMAND_WORD = "maxborrowed"; + public static final String COMMAND_SHORTCUT = "mb"; + public static final String MESSAGE_SUCCESS_FOUND = "The contact to which you owe the most money is: "; + public static final String MESSAGE_SUCCESS_NO_RESULT = "Good news! You don't owe any money."; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds the person to which you owe the most money "; + private CommandResult result; + + public CommandResult getResult() { + return result; + } + + public void setResult(CommandResult result) { + this.result = result; + } + @Override + public CommandResult execute() { + List lastShownList = model.getFilteredPersonList(); + Index index = Index.fromZeroBased(0); + double lowestDebt = 0.0; + + for (int i = 0; i < lastShownList.size(); i++) { + Person person = lastShownList.get(i); + if (person.getMoney().balance < lowestDebt) { + index = Index.fromZeroBased(i); + lowestDebt = person.getMoney().balance; + } + if (lowestDebt == 0.0) { + result = new CommandResult(MESSAGE_SUCCESS_NO_RESULT); + } else { + EventsCenter.getInstance().post(new JumpToListRequestEvent(index)); + result = new CommandResult(MESSAGE_SUCCESS_FOUND + lastShownList.get(index.getZeroBased()).getName()); + } + } + return result; + } +} +``` +###### /java/seedu/address/logic/commands/SearchTagCommand.java +``` java +package seedu.address.logic.commands; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; + +import java.util.Set; +import java.util.function.Predicate; + +import seedu.address.model.person.Person; +import seedu.address.model.tag.Tag; + +/** + * Lists all the people that contain specified tags. + */ +public class SearchTagCommand extends UndoableCommand { + + public static final String COMMAND_WORD = "searchtag"; + public static final String COMMAND_SHORTCUT = "st"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": finds all the people having the specified tags. " + + "Person must have all the provided tags to be selected. " + + PREFIX_TAG + "TAG...\n" + + "Example: " + COMMAND_WORD + + PREFIX_TAG + "owesMoney " + + PREFIX_TAG + "friends"; + + public static final String MESSAGE_SUCCESS = "Found Persons with tags: "; + public static final String MESSAGE_FAILURE = "No results found with the tag: "; + + private final Set tagsToFind; + + /** + * This returns a SearchTagCommand that is ready to be executed. + * @param tags that need to be colored + */ + public SearchTagCommand(Set tags) { + this.tagsToFind = tags; + } + + public Set getTagsToFind() { + return tagsToFind; + } + + /** + * This command lists all the persons which match the search criteria provided by the user. + * @return + */ + @Override + public CommandResult executeUndoableCommand() { + model.updateFilteredPersonList(personHasTags(tagsToFind)); + int result = model.getFilteredPersonList().size(); + String tagsFormatted = formatTagsFeedback(tagsToFind); + if (result > 0) { + return new CommandResult(MESSAGE_SUCCESS + + "\n" + + tagsFormatted + + "\n" + + getMessageForPersonListShownSummary(result)); + } else { + return new CommandResult(MESSAGE_FAILURE + + "\n" + + tagsFormatted); + } + } + + /** + * This function returns person Predicate that indicates if a given has the tags we are + * looking for. + * @return + */ + public static Predicate personHasTags(Set tagsToCheck) { + return person -> person.getTags().containsAll(tagsToCheck); + } + + /** + * Formats the tags to a string to display clearly to user. + * @param tagsToFormat + * @return + */ + public static String formatTagsFeedback(Set tagsToFormat) { + String tagsFormatted = tagsToFormat.toString() + .replace("[", " ") + .replace("]", " ") + .replace("[,", " "); + return tagsFormatted; + } +} + + + + + + +``` +###### /java/seedu/address/logic/parser/AddressBookParser.java +``` java + case BalanceCommand.COMMAND_SHORTCUT: + return new BalanceCommand(); + + case BalanceCommand.COMMAND_WORD: + return new BalanceCommand(); +``` +###### /java/seedu/address/logic/parser/AddressBookParser.java +``` java + case MinCommand.COMMAND_WORD: + return new MinCommand(); + + case MinCommand.COMMAND_SHORTCUT: + return new MinCommand(); +``` +###### /java/seedu/address/logic/parser/AddressBookParser.java +``` java + case MapCommand.COMMAND_WORD: + return new MapCommand(); + case MapCommand.COMMAND_SHORTCUT: + return new MapCommand(); +``` +###### /java/seedu/address/logic/parser/AddressBookParser.java +``` java + case SearchTagCommand.COMMAND_WORD: + return new SearchTagCommandParser().parse(arguments); + case SearchTagCommand.COMMAND_SHORTCUT: + return new SearchTagCommandParser().parse(arguments); +``` +###### /java/seedu/address/logic/parser/CurrencyCommandParser.java +``` java + Currency fromFormattedCurrency = Currency.get(fromCurrency); + Currency toFormattedCurrency = Currency.get(toCurrency); + + if (fromFormattedCurrency == null || toFormattedCurrency == null) { + throw new ParseException(CurrencyCommand.MESSAGE_CURRENCY_NOT_SUPPORTED); + } + return new CurrencyCommand(currencyIndex, fromCurrency, toCurrency); + } catch (IllegalValueException ive) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, CurrencyCommand.MESSAGE_USAGE)); + } catch (NumberFormatException nfe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, CurrencyCommand.MESSAGE_USAGE)); + } catch (IllegalArgumentException iae) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, CurrencyCommand.MESSAGE_USAGE)); + } catch (ArrayIndexOutOfBoundsException aiobe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, CurrencyCommand.MESSAGE_USAGE)); + } + } + +} + + + +``` +###### /java/seedu/address/logic/parser/SearchTagCommandParser.java +``` java + +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.logic.parser.ParserUtil.parseTags; + +import java.util.Set; + +import seedu.address.commons.exceptions.IllegalValueException; + +import seedu.address.logic.commands.SearchTagCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.tag.Tag; + +/** + * Parses a SearchTagCommand. Verifies that tags are properly formatted before + */ +public class SearchTagCommandParser implements Parser { + + public static final String MESSAGE_INVALID_COMMAND_NO_TAGS = "Please insert tags in the tag field: t/"; + + /** + * Parses the given {@code String} of arguments in the context of the ColorCommand + * and returns an RemoveTagCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + + public SearchTagCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_TAG); + Set tagsToFind; + try { + tagsToFind = parseTags(argMultimap.getAllValues(PREFIX_TAG)); + if (tagsToFind.isEmpty()) { + throw new ParseException(MESSAGE_INVALID_COMMAND_NO_TAGS); + } + } catch (IllegalValueException ive) { + throw new ParseException(ive.getMessage(), ive); + } + return new SearchTagCommand(tagsToFind); + } +} +``` +###### /java/seedu/address/model/money/Money.java +``` java + /** + * Limits the input for money to 16 digits. This is when we start the lose precision for balance command. + * @param test + * @return + */ + public static boolean isNumberLowEnough(String test) { + char[] testArray = test.toCharArray(); + int size = testArray.length; + if (size > 16) { + return false; + } + return true; + } + +``` +###### /java/seedu/address/ui/BrowserPanel.java +``` java + public void loadAtmSearchPage() { + loadPage(ATM_SEARCH_PAGE_URL); + } + + private void loadPersonPage(Person person) { + loadPage(SEARCH_PAGE_URL + person.getName().fullName); + } + private void loadPersonAddress(Person person) { + loadPage (ADDRESS_SEARCH_PAGE_URL + person.getAddress().value); + } +``` +###### /java/seedu/address/ui/BrowserPanel.java +``` java + @Subscribe + private void handleShowMapRequestEvent(ShowMapRequestEvent event) { + logger.info(LogsCenter.getEventHandlingLogMessage(event)); + loadAtmSearchPage(); + } + +} +``` diff --git a/collated/functional/chenchongsong.md b/collated/functional/chenchongsong.md new file mode 100644 index 000000000000..3dfbcdcfe3dc --- /dev/null +++ b/collated/functional/chenchongsong.md @@ -0,0 +1,1516 @@ +# chenchongsong +###### /java/seedu/address/logic/commands/ItemAddCommand.java +``` java +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_MONEY; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.item.Item; +import seedu.address.model.money.Money; +import seedu.address.model.person.Address; +import seedu.address.model.person.Email; +import seedu.address.model.person.Name; +import seedu.address.model.person.Person; +import seedu.address.model.person.Phone; +import seedu.address.model.person.exceptions.DuplicatePersonException; +import seedu.address.model.person.exceptions.PersonNotFoundException; +import seedu.address.model.tag.Tag; + +/** + * Attach a new item to a specified person + */ +public class ItemAddCommand extends UndoableCommand { + + public static final String COMMAND_WORD = "itemadd"; + public static final String COMMAND_SHORTCUT = "ia"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Attaching a new item to a specified person. " + + "Parameters: INDEX " + + PREFIX_NAME + "ITEM_NAME " + + PREFIX_MONEY + "MONEY\n" + + "Example: " + COMMAND_WORD + " 2 " + + PREFIX_NAME + "taxiFare " + + PREFIX_MONEY + "30\n"; + + public static final String MESSAGE_ADD_ITEM_SUCCESS = "Item Added for Person %1$s.\n" + + "To view all items, use \"itemshow\" command!"; + public static final String MESSAGE_INVALID_ARGUMENT = "The Argument is Invalid!" + "\n" + + Item.MESSAGE_ITEMNAME_CONSTRAINTS + "\n" + + Item.MESSAGE_ITEMVALUE_CONSTRAINTS; + + private final Index targetIndex; + private final Item item; + private Person personToEdit; + private Person editedPerson; + + /** + * @param index of person in the filtered person list to whom a new item will be attached + */ + public ItemAddCommand(Index index, String itemName, String itemValue) { + requireNonNull(index); + this.targetIndex = index; + this.item = new Item(itemName, itemValue); + } + + @Override + public CommandResult executeUndoableCommand() throws CommandException { + try { + model.updatePerson(personToEdit, editedPerson); + } catch (DuplicatePersonException dpe) { + throw new AssertionError("The target person cannot be duplicate"); + } catch (PersonNotFoundException pnfe) { + throw new AssertionError("The target person cannot be missing"); + } + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + return new CommandResult(String.format(MESSAGE_ADD_ITEM_SUCCESS, targetIndex.getOneBased())); + } + + @Override + protected void preprocessUndoableCommand() throws CommandException { + List lastShownList = model.getFilteredPersonList(); + + if (targetIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + personToEdit = lastShownList.get(targetIndex.getZeroBased()); + editedPerson = getEditedPerson(personToEdit); + } + + /** + * Creates and returns a {@code Person} with the details of {@code person} + * but with a updated item list + */ + private Person getEditedPerson(Person person) { + assert person != null; + + // references the original objects + Name name = person.getName(); + Phone phone = person.getPhone(); + Email email = person.getEmail(); + Address address = person.getAddress(); + Money money = person.getMoney(); + Set tags = person.getTags(); + ArrayList items = getAppendedItemList(person.getItems()); + + // returns a new Person based mainly on references to original information + return new Person(name, phone, email, address, money, tags, items); + } + + /** + * Create and returns an updated Item List + * The new item would be appended to the end of the original Item List + */ + private ArrayList getAppendedItemList(ArrayList items) { + assert items != null; + ArrayList appendedItemList = new ArrayList<>(items); + appendedItemList.add(this.item); + return appendedItemList; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof ItemAddCommand // instanceof handles nulls + && this.targetIndex.equals(((ItemAddCommand) other).targetIndex) + && this.item.equals(((ItemAddCommand) other).item)); // state check + } + +} +``` +###### /java/seedu/address/logic/commands/ItemDeleteCommand.java +``` java +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.item.Item; +import seedu.address.model.money.Money; +import seedu.address.model.person.Address; +import seedu.address.model.person.Email; +import seedu.address.model.person.Name; +import seedu.address.model.person.Person; +import seedu.address.model.person.Phone; +import seedu.address.model.person.exceptions.DuplicatePersonException; +import seedu.address.model.person.exceptions.PersonNotFoundException; +import seedu.address.model.tag.Tag; + +/** + * Delete an item from a specified person + */ +public class ItemDeleteCommand extends UndoableCommand { + + public static final String COMMAND_WORD = "itemdelete"; + public static final String COMMAND_SHORTCUT = "id"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Deleting an item from a specified person.\n" + + "Parameters: PERSON_INDEX ITEM_INDEX\n" + + "PERSON_INDEX and ITEM_INDEX should be POSITIVE integers!\n" + + "Example: " + COMMAND_WORD + " 1 2\n" + + "This example command deletes the second item from the first person.\n"; + + public static final String MESSAGE_ADD_ITEM_SUCCESS = "Items Deleted for Person %1$s.\n"; + public static final String MESSAGE_INVALID_ARGUMENT = "The Argument is Invalid!"; + + private final Index indexPerson; + private final Index indexItem; + private Person personToEdit; + private Person editedPerson; + + /** + * @param indexPerson The index of person in the filtered person list whose item the user wants to delete + * @param indexItem The index of item the user wants to delete + */ + public ItemDeleteCommand(Index indexPerson, Index indexItem) { + requireNonNull(indexPerson); + requireNonNull(indexItem); + this.indexPerson = indexPerson; + this.indexItem = indexItem; + } + + @Override + public CommandResult executeUndoableCommand() throws CommandException { + try { + model.updatePerson(personToEdit, editedPerson); + } catch (DuplicatePersonException dpe) { + throw new AssertionError("The target person cannot be duplicate"); + } catch (PersonNotFoundException pnfe) { + throw new AssertionError("The target person cannot be missing"); + } + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + return new CommandResult(String.format(MESSAGE_ADD_ITEM_SUCCESS, indexPerson.getOneBased())); + } + + @Override + protected void preprocessUndoableCommand() throws CommandException { + List lastShownList = model.getFilteredPersonList(); + + if (indexPerson.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + personToEdit = lastShownList.get(indexPerson.getZeroBased()); + try { + editedPerson = getEditedPerson(personToEdit); + } catch (IllegalValueException ive) { + throw new CommandException(ive.getMessage()); + } + } + + /** + * Creates and returns a {@code Person} with the details of {@code person} + * but with an updated item list + */ + private Person getEditedPerson(Person person) throws IllegalValueException { + assert person != null; + + // references the original objects + Name name = person.getName(); + Phone phone = person.getPhone(); + Email email = person.getEmail(); + Address address = person.getAddress(); + Money money = person.getMoney(); + Set tags = person.getTags(); + + if (indexItem.getZeroBased() >= person.getItems().size()) { + throw new IllegalValueException(Messages.MESSAGE_INVALID_ITEM_INDEX); + } + ArrayList items = getItemRemovedItemList(person.getItems()); + + // returns a new Person based mainly on references to original information, but with an updated item list + return new Person(name, phone, email, address, money, tags, items); + } + + /** + * Create and returns an updated Item List + * where the target Item will be removed + */ + private ArrayList getItemRemovedItemList(ArrayList items) { + assert items != null; + ArrayList itemRemovedItemList = new ArrayList<>(items); + itemRemovedItemList.remove(indexItem.getZeroBased()); + return itemRemovedItemList; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof ItemDeleteCommand // instanceof handles nulls + && this.indexPerson.equals(((ItemDeleteCommand) other).indexPerson) + && this.indexItem.equals(((ItemDeleteCommand) other).indexItem)); // state check + } + +} +``` +###### /java/seedu/address/logic/commands/ItemShowCommand.java +``` java +package seedu.address.logic.commands; + +import java.util.List; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.item.UniqueItemList; +import seedu.address.model.person.Person; + +/** + * Show all items related to a specified person. + * The person is identified using it's last displayed index from the TravleBanker. + */ +public class ItemShowCommand extends Command { + + public static final String COMMAND_WORD = "itemshow"; + public static final String COMMAND_SHORTCUT = "is"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Show all items related to a person (specified by INDEX).\n" + + "Parameters: INDEX (must be a positive integer)\n" + + "Example: " + COMMAND_WORD + " 1"; + + public static final String MESSAGE_SHOW_ITEM_SUCCESS = "Items Showed for Person: %d.\n" + + "Money Due to Unknown Items: %.2f\n"; + + private final Index targetIndex; + + public ItemShowCommand(Index targetIndex) { + this.targetIndex = targetIndex; + } + + @Override + public CommandResult execute() throws CommandException { + + List lastShownList = model.getFilteredPersonList(); + + if (targetIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + Person targetPerson = lastShownList.get(targetIndex.getZeroBased()); + UniqueItemList items = targetPerson.getUniqueItemList(); + Double reasonUnknownAmount = targetPerson.getReasonUnknownAmount(); + return new CommandResult(getResultString(items, reasonUnknownAmount)); + } + + private String getResultString(UniqueItemList items, Double reasonUnknownAmount) { + return String.format(MESSAGE_SHOW_ITEM_SUCCESS, targetIndex.getOneBased(), reasonUnknownAmount) + + items.toString(); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof ItemShowCommand // instanceof handles nulls + && this.targetIndex.equals(((ItemShowCommand) other).targetIndex)); // state check + } +} +``` +###### /java/seedu/address/logic/commands/RemoveTagCommand.java +``` java +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; + +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.money.Money; +import seedu.address.model.person.Address; +import seedu.address.model.person.Email; +import seedu.address.model.person.Name; +import seedu.address.model.person.Person; +import seedu.address.model.person.Phone; +import seedu.address.model.person.exceptions.DuplicatePersonException; +import seedu.address.model.person.exceptions.PersonNotFoundException; +import seedu.address.model.tag.Tag; + +/** + * Edits the details of an existing person in the address book. + */ +public class RemoveTagCommand extends UndoableCommand { + + public static final String COMMAND_WORD = "removetag"; + public static final String COMMAND_SHORTCUT = "rt"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Removes the specific tags of the person identified " + + "by the index number used in the last person listing.\n" + + "Parameters: INDEX (must be a positive integer) " + + "[" + PREFIX_TAG + "TAG]...\n" + + "Example: " + COMMAND_WORD + " 1 " + + PREFIX_TAG + "owesMoney " + + PREFIX_TAG + "friends"; + + public static final String MESSAGE_REMOVE_TAG_SUCCESS = "Remove Tags for Person: %1$s"; + public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book."; + public static final String MESSAGE_TAG_NOT_EXIST = "Your Input Contains Non-existent Tag(s)!"; + + private final Index index; + private final EditPersonDescriptor editPersonDescriptor; + + private Person personToEdit; + private Person editedPerson; + + /** + * @param index of the person in the filtered person list to edit + * @param editPersonDescriptor details to edit the person with + */ + public RemoveTagCommand(Index index, EditPersonDescriptor editPersonDescriptor) { + requireNonNull(index); + requireNonNull(editPersonDescriptor); + + this.index = index; + this.editPersonDescriptor = new EditPersonDescriptor(editPersonDescriptor); + } + + @Override + public CommandResult executeUndoableCommand() throws CommandException { + try { + model.updatePerson(personToEdit, editedPerson); + } catch (DuplicatePersonException dpe) { + throw new CommandException(MESSAGE_DUPLICATE_PERSON); + } catch (PersonNotFoundException pnfe) { + throw new AssertionError("The target person cannot be missing"); + } + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + return new CommandResult(String.format(MESSAGE_REMOVE_TAG_SUCCESS, editedPerson)); + } + + @Override + protected void preprocessUndoableCommand() throws CommandException { + List lastShownList = model.getFilteredPersonList(); + + if (index.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + try { + personToEdit = lastShownList.get(index.getZeroBased()); + editedPerson = createEditedPerson(personToEdit, editPersonDescriptor); + } catch (IllegalArgumentException iae) { + throw new CommandException(MESSAGE_TAG_NOT_EXIST); + } + } + + /** + * Creates and returns a {@code Person} with the details of {@code personToEdit} + * Remove all tags within {@code editPersonDescriptor} from the original tag list of personToEdit + */ + public static Person createEditedPerson( + Person personToEdit, EditPersonDescriptor toBeRemovedTagsDescriptor) throws IllegalArgumentException { + assert personToEdit != null; + + Name updatedName = personToEdit.getName(); + Phone updatedPhone = personToEdit.getPhone(); + Email updatedEmail = personToEdit.getEmail(); + Address updatedAddress = personToEdit.getAddress(); + Money updatedMoney = personToEdit.getMoney(); + + Set toBeRemovedTags = toBeRemovedTagsDescriptor.getTags().orElse(personToEdit.getTags()); + Set originalTags = personToEdit.getTags(); + checkArgument(allTagsExistOriginally(toBeRemovedTags, originalTags), MESSAGE_TAG_NOT_EXIST); + + Set updatedTags = getUpdatedTags(toBeRemovedTags, originalTags); + + return new Person(updatedName, updatedPhone, updatedEmail, updatedAddress, updatedMoney, updatedTags); + } + + public static Set getUpdatedTags(Set toBeRemovedTags, Set originalTags) { + Set updatedTags = new HashSet<>(); + for (Tag t: originalTags) { + if (!toBeRemovedTags.contains(t)) { + updatedTags.add(t); + } + } + return updatedTags; + } + + /** + * make sure all tags originally exist in the person info + * @param toBeRemovedTags + * @param originalTags + * @return + */ + private static boolean allTagsExistOriginally(Set toBeRemovedTags, Set originalTags) { + for (Tag tag: toBeRemovedTags) { + if (!originalTags.contains(tag)) { + return false; + } + } + return true; + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + // instanceof handles nulls + if (!(other instanceof RemoveTagCommand)) { + return false; + } + // state check + RemoveTagCommand e = (RemoveTagCommand) other; + return index.equals(e.index) + && editPersonDescriptor.equals(e.editPersonDescriptor) + && Objects.equals(personToEdit, e.personToEdit); + } + +} +``` +###### /java/seedu/address/logic/commands/SortCommand.java +``` java +package seedu.address.logic.commands; + +/** + * Sort all persons in address book in order. + * Keywords will be given by user through arguments. + * Both ascending and descending order is supported. + */ +public class SortCommand extends Command { + + public static final String COMMAND_WORD = "sort"; + public static final String COMMAND_SHORTCUT = "so"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Sorts all persons in ascendingly or descendingly, " + + "ordering by the specified keywords.\n" + + "Parameters: KEYWORD_PREFIX/ORDER ...\n" + + "Example1: " + COMMAND_WORD + " n/desc\n" + + "Example2: " + COMMAND_WORD + " m/asc"; + + public static final String MESSAGE_SUCCESS = "Sorted all persons"; + + public static final String SORT_ORDER_ASCENDING = "asc"; + public static final String SORT_ORDER_DESCENDING = "desc"; + + public final String sortKey; + public final String sortOrder; + + public SortCommand(String key, String order) { + this.sortKey = key; + this.sortOrder = order; + } + + @Override + public CommandResult execute() { + model.sortUniquePersonList(sortKey, sortOrder); + return new CommandResult(MESSAGE_SUCCESS); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof SortCommand // instanceof handles nulls + && this.sortKey.equals(((SortCommand) other).sortKey) + && this.sortOrder.equals(((SortCommand) other).sortOrder)); // state check + } + +} +``` +###### /java/seedu/address/logic/commands/SplitCommand.java +``` java +package seedu.address.logic.commands; + +import static java.lang.Math.round; +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_MONEY; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.commons.util.CollectionUtil; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.money.Money; +import seedu.address.model.person.Address; +import seedu.address.model.person.Email; +import seedu.address.model.person.Name; +import seedu.address.model.person.Person; +import seedu.address.model.person.Phone; +import seedu.address.model.person.exceptions.DuplicatePersonException; +import seedu.address.model.person.exceptions.PersonNotFoundException; +import seedu.address.model.tag.Tag; + +/** + * Split a bill evenly among selected people. + */ +public class SplitCommand extends UndoableCommand { + + public static final String COMMAND_WORD = "split"; + public static final String COMMAND_SHORTCUT = "sp"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Split a bill evenly among specified people. " + + "Parameters: INDEX1 [INDEX2...] " + + PREFIX_MONEY + "MONEY\n" + + "Example1: " + COMMAND_WORD + " 1 2 " + + PREFIX_MONEY + "200\n" + + "Example2: " + COMMAND_SHORTCUT + " 1 2 3 " + + PREFIX_MONEY + "400.00"; + + public static final String MESSAGE_SPLIT_BILL_SUCCESS = "Bill Split Successfully Among Selected People!"; + public static final String MESSAGE_DUPLICATE_INDEX = "Duplicate Indices Found in Parameters!"; + + private final ArrayList indices; + private ArrayList peopleToEdit; + private ArrayList editedPeople; + private final double bill; + + /** + * @param indices of people in the filtered person list to settle the bill + */ + public SplitCommand(ArrayList indices, double bill) { + requireNonNull(indices); + this.indices = indices; + peopleToEdit = new ArrayList<>(); + editedPeople = new ArrayList<>(); + this.bill = bill; + } + + @Override + public CommandResult executeUndoableCommand() throws CommandException { + try { + for (int i = 0; i < indices.size(); i++) { + model.updatePerson(peopleToEdit.get(i), editedPeople.get(i)); + } + } catch (DuplicatePersonException dpe) { + throw new AssertionError("The target people cannot be duplicate"); + } catch (PersonNotFoundException pnfe) { + throw new AssertionError("The target people cannot be missing"); + } + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + return new CommandResult(MESSAGE_SPLIT_BILL_SUCCESS); + } + + @Override + protected void preprocessUndoableCommand() throws CommandException { + + if (!CollectionUtil.elementsAreUnique(indices.stream() + .map(Index->Index.getZeroBased()) + .collect(Collectors.toList()))) { + throw new CommandException(MESSAGE_DUPLICATE_INDEX); + } + + List lastShownList = model.getFilteredPersonList(); + + for (Index index : indices) { + if (index.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + Person person = lastShownList.get(index.getZeroBased()); + peopleToEdit.add(person); + editedPeople.add(getSettledPerson(person)); + } + } + + /** + * Creates and returns a {@code Person} with the details of {@code personToEdit} + * but with a updated balance + */ + private Person getSettledPerson(Person personToEdit) { + assert personToEdit != null; + + Name name = personToEdit.getName(); + Phone phone = personToEdit.getPhone(); + Email email = personToEdit.getEmail(); + Address address = personToEdit.getAddress(); + Money money = getSettledMoney(personToEdit.getMoney()); + Set tags = personToEdit.getTags(); + + return new Person(name, phone, email, address, money, tags); + } + + /** + * Create and returns an updated Money Info + * The updated Money would be rounded to 2 decimal places + */ + private Money getSettledMoney(Money moneyToEdit) { + assert moneyToEdit != null; + double updatedBalance = moneyToEdit.toDouble() + bill / indices.size(); + updatedBalance = round(updatedBalance * 100.00) / 100.00; + return new Money(Double.toString(updatedBalance)); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof SplitCommand)) { + return false; + } + + // state check + SplitCommand o = (SplitCommand) other; + + return bill == o.bill + && indices.equals(o.indices); + } + +} +``` +###### /java/seedu/address/logic/parser/AddressBookParser.java +``` java + case RemoveTagCommand.COMMAND_WORD: + return new RemoveTagCommandParser().parse(arguments); + + case RemoveTagCommand.COMMAND_SHORTCUT: + return new RemoveTagCommandParser().parse(arguments); + + case SplitCommand.COMMAND_WORD: + return new SplitCommandParser().parse(arguments); + + case SplitCommand.COMMAND_SHORTCUT: + return new SplitCommandParser().parse(arguments); + + case SortCommand.COMMAND_WORD: + return new SortCommandParser().parse(arguments); + + case SortCommand.COMMAND_SHORTCUT: + return new SortCommandParser().parse(arguments); + + case ItemShowCommand.COMMAND_WORD: + return new ItemShowCommandParser().parse(arguments); + + case ItemShowCommand.COMMAND_SHORTCUT: + return new ItemShowCommandParser().parse(arguments); + + case ItemAddCommand.COMMAND_WORD: + return new ItemAddCommandParser().parse(arguments); + + case ItemAddCommand.COMMAND_SHORTCUT: + return new ItemAddCommandParser().parse(arguments); + + case ItemDeleteCommand.COMMAND_WORD: + return new ItemDeleteCommandParser().parse(arguments); + + case ItemDeleteCommand.COMMAND_SHORTCUT: + return new ItemDeleteCommandParser().parse(arguments); +``` +###### /java/seedu/address/logic/parser/ItemAddCommandParser.java +``` java +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_MONEY; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.logic.commands.ItemAddCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new ItemAddCommand object + */ +public class ItemAddCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the ItemAddCommand + * and returns an ItemAddCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public ItemAddCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_MONEY); + + Index index; + + try { + index = ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (IllegalValueException ive) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, ItemAddCommand.MESSAGE_USAGE)); + } + + if (!ParserUtil.arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_MONEY)) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, ItemAddCommand.MESSAGE_USAGE)); + } + + try { + return new ItemAddCommand(index, + argMultimap.getValue(PREFIX_NAME).get(), argMultimap.getValue(PREFIX_MONEY).get()); + } catch (IllegalArgumentException iae) { + throw new ParseException(ItemAddCommand.MESSAGE_INVALID_ARGUMENT); + } + } + +} +``` +###### /java/seedu/address/logic/parser/ItemDeleteCommandParser.java +``` java +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.logic.commands.ItemDeleteCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new ItemDeleteCommand object. + * {@code indexPerson} represents the index of the person whose item the user want to delete. + * {@code indexItem} represents the index of the item that the user want to delete. + */ +public class ItemDeleteCommandParser implements Parser { + + private Index indexPerson; + private Index indexItem; + + /** + * Parses the given {@code String} of arguments in the context of the ItemDeleteCommand + * and returns an ItemDeleteCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public ItemDeleteCommand parse(String args) throws ParseException { + try { + parsePersonItemIndices(args); + return new ItemDeleteCommand(indexPerson, indexItem); + } catch (IllegalValueException ive) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, ItemDeleteCommand.MESSAGE_USAGE)); + } + } + + /** + * Parses the given {@code String} of two indices into indexPerson and indexItem. + * @param args A string of two indices separated by a whitespace + * @throws IllegalValueException + */ + private void parsePersonItemIndices(String args) throws IllegalValueException { + String[] indices = args.trim().split(" "); + if (indices.length != 2) { + throw new IllegalValueException(ParserUtil.MESSAGE_INVALID_INDEX); + } + indexPerson = ParserUtil.parseIndex(indices[0]); + indexItem = ParserUtil.parseIndex(indices[1]); + } + +} +``` +###### /java/seedu/address/logic/parser/ItemShowCommandParser.java +``` java +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.logic.commands.ItemShowCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new ItemShowCommand object + */ +public class ItemShowCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the ItemShowCommand + * and returns an ItemShowCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public ItemShowCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new ItemShowCommand(index); + } catch (IllegalValueException ive) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, ItemShowCommand.MESSAGE_USAGE)); + } + } +} +``` +###### /java/seedu/address/logic/parser/ParserUtil.java +``` java + /** + * Parses {@code oneBasedIndices} into an ArrayList of {@code Index} and returns it. + * Leading and trailing whitespaces will be trimmed. + * @throws IllegalValueException if the specified index is invalid (not non-zero unsigned integer). + */ + public static ArrayList parseIndices(String oneBasedIndices) throws IllegalValueException { + String[] splittedIndices = oneBasedIndices.trim().split(" "); + ArrayList indices = new ArrayList<>(); + for (String indexString : splittedIndices) { + if (!StringUtil.isNonZeroUnsignedInteger(indexString)) { + throw new IllegalValueException(MESSAGE_INVALID_INDEX); + } + indices.add(Index.fromOneBased(Integer.parseInt(indexString))); + } + return indices; + } + + /** + * Parses {@code args} into an {@code sortKey} and returns it. Leading and trailing whitespaces will be + * trimmed. + * @throws IllegalValueException if the specified SortKey is invalid (not non-zero unsigned integer). + */ + public static String parseSortKey(String args) throws IllegalValueException { + String[] splittedArgs = args.trim().split("/"); + String sortKey = splittedArgs[0] + "/"; + if (splittedArgs.length != 2) { + throw new IllegalValueException(MESSAGE_INVALID_ARGS); + } + if (!sortKey.equals(PREFIX_NAME.getPrefix()) + && !sortKey.equals(PREFIX_PHONE.getPrefix()) + && !sortKey.equals(PREFIX_EMAIL.getPrefix()) + && !sortKey.equals(PREFIX_ADDRESS.getPrefix()) + && !sortKey.equals(PREFIX_TAG.getPrefix()) + && !sortKey.equals(PREFIX_MONEY.getPrefix())) { + throw new IllegalValueException(MESSAGE_INVALID_ARGS); + } + return sortKey; + } + + /** + * Parses {@code args} into an {@code sortOrder} and returns it. Leading and trailing whitespaces will be + * trimmed. + * @throws IllegalValueException if the specified SortOrder is invalid (not non-zero unsigned integer). + */ + public static String parseSortOrder(String args) throws IllegalValueException { + String[] splittedArgs = args.trim().split("/"); + String sortKey = splittedArgs[1]; + if (splittedArgs.length != 2) { + throw new IllegalValueException(MESSAGE_INVALID_ARGS); + } + if ((!sortKey.equals(SORT_ORDER_ASCENDING) && !sortKey.equals(SORT_ORDER_DESCENDING))) { + throw new IllegalValueException(MESSAGE_INVALID_ARGS); + } + return sortKey; + } +``` +###### /java/seedu/address/logic/parser/RemoveTagCommandParser.java +``` java +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; + +import java.util.Collection; +import java.util.Collections; +import java.util.Optional; +import java.util.Set; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; +import seedu.address.logic.commands.RemoveTagCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.tag.Tag; + +/** + * Parses input arguments and creates a new RemoveTagCommand object + */ +public class RemoveTagCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the RemoveTagCommand + * and returns an RemoveTagCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public RemoveTagCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_TAG); + Index index; + + try { + index = ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (IllegalValueException ive) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, RemoveTagCommand.MESSAGE_USAGE)); + } + + EditPersonDescriptor editPersonDescriptor = new EditPersonDescriptor(); + try { + parseTagsForRemove(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(editPersonDescriptor::setTags); + } catch (IllegalValueException ive) { + throw new ParseException(ive.getMessage(), ive); + } + + return new RemoveTagCommand(index, editPersonDescriptor); + } + + /** + * Parses {@code Collection tags} into a {@code Set} if {@code tags} is non-empty. + * If {@code tags} contain only one element which is an empty string, it will be parsed into a + * {@code Set} containing zero tags. + */ + private Optional> parseTagsForRemove(Collection tags) throws IllegalValueException { + assert tags != null; + + if (tags.isEmpty()) { + throw new IllegalValueException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, RemoveTagCommand.MESSAGE_USAGE)); + } + Collection tagSet = tags.size() == 1 && tags.contains("") ? Collections.emptySet() : tags; + return Optional.of(ParserUtil.parseTags(tagSet)); + } + +} +``` +###### /java/seedu/address/logic/parser/SortCommandParser.java +``` java +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.logic.commands.SortCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new SortCommand object + */ +public class SortCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the SortCommand + * and returns an SortCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public SortCommand parse(String args) throws ParseException { + requireNonNull(args); + try { + String sortKey = ParserUtil.parseSortKey(args); + String sortOrder = ParserUtil.parseSortOrder(args); // either "asc" or "desc" + return new SortCommand(sortKey, sortOrder); + } catch (IllegalValueException ive) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, SortCommand.MESSAGE_USAGE)); + } + } + +} +``` +###### /java/seedu/address/logic/parser/SplitCommandParser.java +``` java +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_MONEY; + +import java.util.ArrayList; +import java.util.Optional; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.logic.commands.SplitCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new SplitCommand object + */ +public class SplitCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the SplitCommand + * and returns a SplitCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public SplitCommand parse(String args) throws ParseException { + + requireNonNull(args); + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_MONEY); + + ArrayList indices; + try { + indices = ParserUtil.parseIndices(argMultimap.getPreamble()); + } catch (IllegalValueException ive) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, SplitCommand.MESSAGE_USAGE)); + } + + double bill; + try { + Optional money = argMultimap.getValue(PREFIX_MONEY); + bill = Double.parseDouble(money.get()); + if (bill < 0) { + throw new Exception("Negative Value Detected"); + } + } catch (Exception e) { + throw new ParseException("A correct POSITIVE number(money) needs to be provided for the Bill!"); + } + + return new SplitCommand(indices, bill); + } + +} +``` +###### /java/seedu/address/model/AddressBook.java +``` java + public void sortPersons(String sortKey, String sortOrder) { + persons.sortPersons(sortKey, sortOrder); + } +``` +###### /java/seedu/address/model/item/Item.java +``` java +package seedu.address.model.item; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents a Item in the TravelBanker. + * Guarantees: immutable; name is valid as declared in {@link #isValidItemName(String)} + */ +public class Item { + + public static final String MESSAGE_ITEMNAME_CONSTRAINTS = "ItemNames should be alphanumeric with whitespaces or _"; + public static final String MESSAGE_ITEMVALUE_CONSTRAINTS = "ItemValues can be integers or floating point numbers!"; + public static final String ITEM_NAME_VALIDATION_REGEX = "[\\p{Alnum}_\\s]+"; + public static final String ITEM_VALUE_VALIDATION_REGEX = "-?\\d+(\\.\\d+)?(E-?\\d+)?"; + + private final String itemName; + private final String itemValue; + + /** + * Constructs a {@code Item}. + * + * @param itemName A valid item name. + */ + public Item(String itemName, String itemValue) { + requireNonNull(itemName); + requireNonNull(itemValue); + checkArgument(isValidItemName(itemName), MESSAGE_ITEMNAME_CONSTRAINTS); + checkArgument(isValidItemValue(itemValue), MESSAGE_ITEMVALUE_CONSTRAINTS); + this.itemName = itemName; + this.itemValue = itemValue; + } + + /** + * Returns true if a given string is a valid item name. + */ + public static boolean isValidItemName(String test) { + return test.matches(ITEM_NAME_VALIDATION_REGEX); + } + + /** + * Returns true if a given string is a valid item value. + */ + public static boolean isValidItemValue(String test) { + return test.matches(ITEM_VALUE_VALIDATION_REGEX); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Item // instanceof handles nulls + && this.itemName.equals(((Item) other).itemName) + && this.itemValue.equals(((Item) other).itemValue)); // state check + } + + @Override + public int hashCode() { + return itemName.hashCode(); + } + + /** + * Format state as text for viewing. + */ + public String toString() { + return "ItemName [ " + itemName + " ] || ItemValue [ " + itemValue + " ]"; + } + + public String getItemName() { + return itemName; + } + + public String getItemValue() { + return itemValue; + } + +} +``` +###### /java/seedu/address/model/item/UniqueItemList.java +``` java +package seedu.address.model.item; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; + +import seedu.address.commons.exceptions.DuplicateDataException; +import seedu.address.commons.util.CollectionUtil; + +/** + * A list of items that enforces no nulls and uniqueness between its elements. + * + * Supports minimal set of list operations for the app's features. + * + */ +public class UniqueItemList implements Iterable { + + private ArrayList internalList = new ArrayList<>(); + + /** + * Constructs empty ItemList. + */ + public UniqueItemList() {} + + /** + * Creates a UniqueItemList using given items. + * Enforces no nulls. + */ + public UniqueItemList(ArrayList items) { + requireAllNonNull(items); + internalList.addAll(items); + + assert CollectionUtil.elementsAreUnique(internalList); + } + + /** + * Returns all items in this list as a ArrayList. + * This ArrayList is mutable and change-insulated against the internal list. + */ + public ArrayList toArrayList() { + assert CollectionUtil.elementsAreUnique(internalList); + return new ArrayList<>(internalList); + } + + /** + * Returns all items in this list as a String. + */ + public String toString() { + assert CollectionUtil.elementsAreUnique(internalList); + final StringBuilder builder = new StringBuilder(); + builder.append("Items: "); + for (int i = 0; i < internalList.size(); i++) { + builder.append("\nItem No." + Integer.toString(i + 1) + " || "); + builder.append(internalList.get(i).toString()); + } + return builder.toString(); + } + + /** + * Replaces the Items in this list with those in the argument Item list. + */ + public void setItems(ArrayList newItemList) { + requireAllNonNull(newItemList); + internalList = new ArrayList<>(newItemList); + assert CollectionUtil.elementsAreUnique(internalList); + } + + /** + * Ensures every item in the argument list exists in this object. + */ + public void mergeFrom(UniqueItemList from) { + final ArrayList alreadyInside = this.toArrayList(); + from.internalList.stream() + .filter(item -> !alreadyInside.contains(item)) + .forEach(internalList::add); + + assert CollectionUtil.elementsAreUnique(internalList); + } + + /** + * Returns true if the list contains an equivalent Item as the given argument. + */ + public boolean contains(Item toCheck) { + requireNonNull(toCheck); + return internalList.contains(toCheck); + } + + /** + * Adds a Item to the list. + * + * @throws DuplicateItemException if the Item to add is a duplicate of an existing Item in the list. + */ + public void add(Item toAdd) throws DuplicateItemException { + requireNonNull(toAdd); + if (contains(toAdd)) { + throw new DuplicateItemException(); + } + internalList.add(toAdd); + + assert CollectionUtil.elementsAreUnique(internalList); + } + + @Override + public Iterator iterator() { + assert CollectionUtil.elementsAreUnique(internalList); + return internalList.iterator(); + } + + @Override + public boolean equals(Object other) { + assert CollectionUtil.elementsAreUnique(internalList); + return other == this // short circuit if same object + || (other instanceof UniqueItemList // instanceof handles nulls + && this.internalList.equals(((UniqueItemList) other).internalList)); + } + + /** + * Returns true if the element in this list is equal to the elements in {@code other}. + * The elements do not have to be in the same order. + */ + public boolean equalsOrderInsensitive(UniqueItemList other) { + assert CollectionUtil.elementsAreUnique(internalList); + assert CollectionUtil.elementsAreUnique(other.internalList); + return this == other || new HashSet<>(this.internalList).equals(new HashSet<>(other.internalList)); + } + + @Override + public int hashCode() { + assert CollectionUtil.elementsAreUnique(internalList); + return internalList.hashCode(); + } + + /** + * returns the sum of all items in the internalList + * @return + */ + public double getValueSum() { + double sum = 0.0; + for (Item item: internalList) { + sum += Double.parseDouble(item.getItemValue()); + } + return sum; + } + + /** + * Signals that an operation would have violated the 'no duplicates' property of the list. + */ + public static class DuplicateItemException extends DuplicateDataException { + protected DuplicateItemException() { + super("Operation would result in duplicate items"); + } + } + +} +``` +###### /java/seedu/address/model/Model.java +``` java + /** + * Sorts the person list by a given keyword {} + * The person list would be sorted ascendingly or descendingly, depending on {} + */ + void sortUniquePersonList(String sortKey, String sortOrder); +``` +###### /java/seedu/address/model/ModelManager.java +``` java + @Override + public void sortUniquePersonList(String sortKey, String sortOrder) { + addressBook.sortPersons(sortKey, sortOrder); + } +``` +###### /java/seedu/address/model/person/Person.java +``` java + /** + * Every field must be present and not null. + * @param items must be provided + */ + public Person(Name name, Phone phone, Email email, Address address, Money balance, + Set tags, ArrayListitems) { + requireAllNonNull(name, phone, email, address, tags, items); + this.name = name; + this.phone = phone; + this.email = email; + this.address = address; + this.money = balance; + // protect internal tags from changes in the arg list + this.tags = new UniqueTagList(tags); + this.items = new UniqueItemList(items); + } +``` +###### /java/seedu/address/model/person/Person.java +``` java + /** + * Returns the amount of money due to unknown reasons/items + * @return + */ + public Double getReasonUnknownAmount() { + return money.toDouble() - items.getValueSum(); + } + + /** + * Returns an immutable tag set, which throws {@code UnsupportedOperationException} + * if modification is attempted. + */ + public Set getTags() { + return Collections.unmodifiableSet(tags.toSet()); + } + + public ArrayList getItems() { + return items.toArrayList(); + } + + public UniqueItemList getUniqueItemList() { + return items; + } + + public void setItems(ArrayList items) { + this.items.setItems(items); + } + + public void clearItems() { + this.items.setItems(new ArrayList<>()); + } +``` +###### /java/seedu/address/model/person/Person.java +``` java + /** + * Create comparator for sorting person list + * @param sortKey + * @param sortOrder either "asc" or "desc" + * @return comparator + */ + public static Comparator createComparator(String sortKey, String sortOrder) { + + Comparator comparator; + if (sortKey.equals(PREFIX_NAME.getPrefix())) { + comparator = (person1, person2) -> +person1.getName().compareTo(person2.getName()); + } else if (sortKey.equals(PREFIX_PHONE.getPrefix())) { + comparator = (person1, person2) -> +person1.getPhone().compareTo(person2.getPhone()); + } else if (sortKey.equals(PREFIX_EMAIL.getPrefix())) { + comparator = (person1, person2) -> +person1.getEmail().compareTo(person2.getEmail()); + } else if (sortKey.equals(PREFIX_ADDRESS.getPrefix())) { + comparator = (person1, person2) -> +person1.getAddress().compareTo(person2.getAddress()); + } else if (sortKey.equals(PREFIX_MONEY.getPrefix())) { + comparator = (person1, person2) -> +person1.getMoney().compareTo(person2.getMoney()); + } else if (sortKey.equals(PREFIX_TAG.getPrefix())) { + comparator = (person1, person2) -> +Integer.compare(person1.getTags().size(), person2.getTags().size()); + } else { + // sort name by default + comparator = (person1, person2) -> +person1.getName().toString() + .compareToIgnoreCase(person2.getName().toString()); + } + + if (sortOrder.equals(SORT_ORDER_DESCENDING)) { + comparator = comparator.reversed(); + } + return comparator; + } +``` +###### /java/seedu/address/model/person/UniquePersonList.java +``` java + /** + * Sorts {@code internalList} by keyword ascendingly or descendingly + */ + public void sortPersons(String sortKey, String sortOrder) { + Comparator comparator = Person.createComparator(sortKey, sortOrder); + internalList.sort(comparator); + } +``` +###### /java/seedu/address/storage/XmlAdaptedItem.java +``` java +package seedu.address.storage; + +import javax.xml.bind.annotation.XmlElement; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.item.Item; + +/** + * JAXB-friendly adapted version of the Item. + */ +public class XmlAdaptedItem { + + @XmlElement(required = true) + private String name; + @XmlElement(required = true) + private String value; + + /** + * Constructs an XmlAdaptedItem. + * This is the no-arg constructor that is required by JAXB. + */ + public XmlAdaptedItem() {} + + /** + * Constructs a {@code XmlAdaptedItem} with the given {@code itemName}. + */ + public XmlAdaptedItem(String itemName, String itemValue) { + this.name = itemName; + this.value = itemValue; + } + + /** + * Converts a given item into this class for JAXB use. + * + * @param source future changes to this will not affect the created + */ + public XmlAdaptedItem(Item source) { + name = source.getItemName(); + value = source.getItemValue(); + } + + /** + * Converts this jaxb-friendly adapted item object into the model's Item object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted person + */ + public Item toModelType() throws IllegalValueException { + if (!Item.isValidItemName(name)) { + throw new IllegalValueException(Item.MESSAGE_ITEMNAME_CONSTRAINTS); + } + if (!Item.isValidItemValue(value)) { + throw new IllegalValueException(Item.MESSAGE_ITEMVALUE_CONSTRAINTS); + } + return new Item(name, value); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (!(other instanceof XmlAdaptedItem)) { + return false; + } + return name.equals(((XmlAdaptedItem) other).name); + } + +} +``` +###### /java/seedu/address/storage/XmlAdaptedPerson.java +``` java + @XmlElement + private List items = new ArrayList<>(); +``` +###### /java/seedu/address/storage/XmlAdaptedPerson.java +``` java + final List personItems = new ArrayList<>(); + for (XmlAdaptedItem item : items) { + personItems.add(item.toModelType()); + } +``` +###### /java/seedu/address/storage/XmlAdaptedPerson.java +``` java + final ArrayList items = new ArrayList<>(personItems); + return new Person(name, phone, email, address, balance, tags, items); +``` diff --git a/collated/functional/pkuhanan.md b/collated/functional/pkuhanan.md new file mode 100644 index 000000000000..0604f2883d8b --- /dev/null +++ b/collated/functional/pkuhanan.md @@ -0,0 +1,427 @@ +# pkuhanan +###### /java/seedu/address/logic/commands/EditCommand.java +``` java + public void setMoney(Money money) { + this.money = money; + } + + public Optional getMoney() { + return Optional.ofNullable(money); + } +``` +###### /java/seedu/address/logic/commands/MaxCommand.java +``` java +package seedu.address.logic.commands; + +import java.util.List; + +import seedu.address.commons.core.EventsCenter; +import seedu.address.commons.core.index.Index; + +import seedu.address.commons.events.ui.JumpToListRequestEvent; +import seedu.address.model.person.Person; + +/** + * Finds the person that owes you the most money. + */ +public class MaxCommand extends Command { + public static final String COMMAND_WORD = "maxlent"; + public static final String COMMAND_SHORTCUT = "ml"; + public static final String MESSAGE_SUCCESS = "The contact who owes you the most money is: "; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds the person that owes the most money. " + + "If two contacts owe the same amount, only one will be selected."; + + @Override + public CommandResult execute() { + List lastShownList = model.getFilteredPersonList(); + Index index = Index.fromZeroBased(0); + Double highestDebt = 0.0; + + for (int i = 0; i < lastShownList.size(); i++) { + Person person = lastShownList.get(i); + if (person.getMoney().balance > highestDebt) { + index = Index.fromZeroBased(i); + highestDebt = person.getMoney().balance; + } + } + + EventsCenter.getInstance().post(new JumpToListRequestEvent(index)); + return new CommandResult(MESSAGE_SUCCESS + lastShownList.get(index.getZeroBased()).getName()); + } +} +``` +###### /java/seedu/address/logic/commands/RemindCommand.java +``` java +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.awt.Desktop; +import java.net.URI; +import java.util.List; + +import seedu.address.commons.core.EventsCenter; +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.commons.events.ui.JumpToListRequestEvent; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.person.Person; + + +/** + * Helps the user send a reminder by email to the contact + */ +public class RemindCommand extends Command { + public static final String COMMAND_WORD = "remind"; + public static final String COMMAND_SHORTCUT = "rm"; + public static final String MESSAGE_SUCCESS = "Generated email for contact: "; + public static final String MESSAGE_FAILURE = " does not owe you any money"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds the person that owes the most money "; + + private final Index index; + private Person person; + + /** + * @param index of the person in the filtered person list to remind + */ + public RemindCommand(Index index) { + requireNonNull(index); + + this.index = index; + } + + @Override + public CommandResult execute() throws CommandException { + List lastShownList = model.getFilteredPersonList(); + if (index.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + person = lastShownList.get(index.getZeroBased()); + EventsCenter.getInstance().post(new JumpToListRequestEvent(index)); + + if (person.getMoney().balance > 0) { + try { + Desktop desktop; + if (Desktop.isDesktopSupported() + && (desktop = Desktop.getDesktop()).isSupported(Desktop.Action.MAIL)) { + String uri = "mailto:" + person.getEmail() + + "?subject=Reminder:%20Please%20pay%20me%20back&body=Please%20pay%20me%20back:%20$" + + person.getMoney(); + URI mailto = new URI(uri); + desktop.mail(mailto); + } else { + throw new AssertionError("No email client configured"); + } + return new CommandResult(MESSAGE_SUCCESS + person.getName() + "<" + person.getEmail() + ">"); + } catch (Exception e) { + throw new AssertionError("Problem sending email"); + } + } else { + return new CommandResult(person.getName() + MESSAGE_FAILURE); + } + } +} +``` +###### /java/seedu/address/logic/commands/TransactionCommand.java +``` java +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; + +import java.util.List; +import java.util.Set; + +import seedu.address.commons.core.EventsCenter; +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.commons.events.ui.JumpToListRequestEvent; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.money.Money; +import seedu.address.model.person.Address; +import seedu.address.model.person.Email; +import seedu.address.model.person.Name; +import seedu.address.model.person.Person; +import seedu.address.model.person.Phone; +import seedu.address.model.person.exceptions.DuplicatePersonException; +import seedu.address.model.person.exceptions.PersonNotFoundException; +import seedu.address.model.tag.Tag; + +/** + * Updates the balance according to the reported transaction + */ +public class TransactionCommand extends UndoableCommand { + public static final String COMMAND_WORD = "transaction"; + public static final String COMMAND_SHORTCUT = "t"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Updates the balance according to the transaction. " + + "Positive means money received from the contact and negative means money paid to the contact " + + "Parameters: INDEX (must be a positive integer) TRANSACTION_AMOUNT (must be an double) "; + + public static final String MESSAGE_TRANSACTION_PERSON_SUCCESS = "Balance updated for: "; + + private final Index index; + private final Double amount; + + private Person personToEdit; + private Person editedPerson; + private String newBalance; + + /** + * @param index of the person in the filtered person list to settle + * @param amount of money paid in the transaction + */ + public TransactionCommand(Index index, Double amount) { + requireNonNull(index); + requireNonNull(amount); + + this.index = index; + this.amount = amount; + } + + @Override + public CommandResult executeUndoableCommand() throws CommandException { + try { + model.updatePerson(personToEdit, editedPerson); + } catch (DuplicatePersonException dpe) { + throw new AssertionError("The target person cannot be duplicate"); + } catch (PersonNotFoundException pnfe) { + throw new AssertionError("The target person cannot be missing"); + } + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + EventsCenter.getInstance().post(new JumpToListRequestEvent(index)); + return new CommandResult(MESSAGE_TRANSACTION_PERSON_SUCCESS + editedPerson.getName()); + } + + @Override + protected void preprocessUndoableCommand() throws CommandException { + List lastShownList = model.getFilteredPersonList(); + + if (index.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + personToEdit = lastShownList.get(index.getZeroBased()); + newBalance = Double.toString(personToEdit.getMoney().balance - amount); + editedPerson = getPersonAfterTransaction(personToEdit, newBalance); + } + + + /** + * Creates and returns a {@code Person} with the details of {@code personToEdit} + * but with a 0 balance + */ + private static Person getPersonAfterTransaction(Person personToEdit, String newBalance) { + assert personToEdit != null; + + Name name = personToEdit.getName(); + Phone phone = personToEdit.getPhone(); + Email email = personToEdit.getEmail(); + Address address = personToEdit.getAddress(); + Money money = new Money(newBalance); + Set tags = personToEdit.getTags(); + + return new Person(name, phone, email, address, money, tags); + } +} +``` +###### /java/seedu/address/logic/parser/AddressBookParser.java +``` java + case MaxCommand.COMMAND_WORD: + return new MaxCommand(); + + case MaxCommand.COMMAND_SHORTCUT: + return new MaxCommand(); +``` +###### /java/seedu/address/logic/parser/AddressBookParser.java +``` java + case SettleCommand.COMMAND_WORD: + return new SettleCommandParser().parse(arguments); + + case SettleCommand.COMMAND_SHORTCUT: + return new SettleCommandParser().parse(arguments); +``` +###### /java/seedu/address/logic/parser/AddressBookParser.java +``` java + case RemindCommand.COMMAND_WORD: + return new RemindCommandParser().parse(arguments); + case RemindCommand.COMMAND_SHORTCUT: + return new RemindCommandParser().parse(arguments); + + case TransactionCommand.COMMAND_WORD: + return new TransactionCommandParser().parse(arguments); + + case TransactionCommand.COMMAND_SHORTCUT: + return new TransactionCommandParser().parse(arguments); +``` +###### /java/seedu/address/logic/parser/ParserUtil.java +``` java + /** + * Parses a {@code String money} into an {@code Money}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws IllegalValueException if the given {@code money} is invalid. + */ + public static Money parseMoney(String money) throws IllegalValueException { + requireNonNull(money); + String trimmedMoney = money.trim(); + if (!Money.isValidMoney(trimmedMoney) || !Money.isNumberLowEnough(money)) { + throw new IllegalValueException(Money.MESSAGE_MONEY_CONSTRAINTS); + } + return new Money(trimmedMoney); + } + + /** + * Parses a {@code Optional money} into an {@code Optional} if {@code money} is present. + * See header comment of this class regarding the use of {@code Optional} parameters. + */ + public static Optional parseMoney(Optional money) throws IllegalValueException { + requireNonNull(money); + return money.isPresent() ? Optional.of(parseMoney(money.get())) : Optional.empty(); + } +``` +###### /java/seedu/address/logic/parser/RemindCommandParser.java +``` java +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.logic.commands.RemindCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new RemindCommand object + */ +public class RemindCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the RemindCommand + * and returns a RemindCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public RemindCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new RemindCommand(index); + } catch (IllegalValueException ive) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, RemindCommand.MESSAGE_USAGE)); + } + } +} +``` +###### /java/seedu/address/logic/parser/TransactionCommandParser.java +``` java +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.model.money.Money.MONEY_VALIDATION_REGEX; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.commons.util.StringUtil; +import seedu.address.logic.commands.TransactionCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new TransactionCommand object + */ +public class TransactionCommandParser { + + public static final String MESSAGE_INVALID_INDEX = "Index is not a non-zero unsigned integer."; + /** + * Parses the given {@code String} of arguments in the context of the TransactionCommand + * and returns a TransactionCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public TransactionCommand parse(String args) throws ParseException { + try { + String trimmedArgs = args.trim(); + String[] currencyKeywords = trimmedArgs.split(" "); + + Index index = Index.fromOneBased(Integer.parseInt(currencyKeywords[0])); + String amount = currencyKeywords[1].toUpperCase(); + + if (!StringUtil.isNonZeroUnsignedInteger(currencyKeywords[0]) || !amount.matches(MONEY_VALIDATION_REGEX)) { + throw new IllegalValueException(MESSAGE_INVALID_INDEX); + } + + return new TransactionCommand(index, Double.parseDouble(amount)); + } catch (IllegalValueException ive) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, TransactionCommand.MESSAGE_USAGE)); + } catch (NumberFormatException nfe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, TransactionCommand.MESSAGE_USAGE)); + } + } +} +``` +###### /java/seedu/address/model/money/Money.java +``` java +package seedu.address.model.money; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents a Person's Money Balance in the TravelBanker. + * Guarantees: immutable; is valid as declared in {@link #isValidMoney(String)} + */ +public class Money { + public static final String MESSAGE_MONEY_CONSTRAINTS = "Money values should be numbers and a maximum " + + "of 16 digits long"; +``` +###### /java/seedu/address/model/money/Money.java +``` java + /** + * Returns true if the user need to pay the contact certain amount of money + * @return true/false + */ + public boolean isNeedPaidMoney() { + return balance < 0.0; + } + + /** + * Returns true if the user need to received certain amount of money from the contact + * @return true/false + */ + public boolean isNeedReceivedMoney() { + return balance > 0.0; + } + +``` +###### /java/seedu/address/model/money/Money.java +``` java + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Money // instanceof handles nulls + && this.value.equals(((Money) other).value)); // state check + } +``` +###### /java/seedu/address/model/person/Person.java +``` java + public Money getMoney() { + return money; + } +``` +###### /java/seedu/address/storage/XmlAdaptedPerson.java +``` java + @XmlElement + private String balance; +``` +###### /java/seedu/address/storage/XmlAdaptedPerson.java +``` java + if (!Money.isValidMoney(this.balance)) { + throw new IllegalValueException(Money.MESSAGE_MONEY_CONSTRAINTS); + } + final Money balance = new Money(this.balance); +``` +###### /java/seedu/address/ui/PersonCard.java +``` java + @FXML + private Label money; +``` diff --git a/collated/functional/software-1234-reused.md b/collated/functional/software-1234-reused.md new file mode 100644 index 000000000000..3d6a4a4aa4f9 --- /dev/null +++ b/collated/functional/software-1234-reused.md @@ -0,0 +1,90 @@ +# software-1234-reused +###### /java/seedu/address/logic/commands/CurrencyCommand.java +``` java +package seedu.address.logic.commands; + +import java.math.BigDecimal; +import java.util.List; + +import org.json.JSONException; + +import com.ritaja.xchangerate.api.CurrencyConverter; +import com.ritaja.xchangerate.api.CurrencyConverterBuilder; +import com.ritaja.xchangerate.api.CurrencyNotSupportedException; +import com.ritaja.xchangerate.endpoint.EndpointException; +import com.ritaja.xchangerate.service.ServiceException; +import com.ritaja.xchangerate.storage.StorageException; +import com.ritaja.xchangerate.util.Currency; +import com.ritaja.xchangerate.util.Strategy; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.person.Person; + +/** + * Lists all the commands entered by user from the start of app launch. + */ +public class CurrencyCommand extends Command { + + public static final String COMMAND_WORD = "convert"; + public static final String COMMAND_SHORTCUT = "cv"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Deletes the person identified by the index number used in the last person listing.\n" + + "Parameters: INDEX (must be a positive integer) " + + "[Current Currency Symbol] " + + "[New Currency Symbol]\n" + + "Example: " + COMMAND_WORD + " 1" + " SGD" + " USD"; + public static final String MESSAGE_SUCCESS = "Here is your balance in the new currency"; + + private String fromCurrency; + private String toCurrency; + private Index index; + private Person convertedPerson; + private Double convertedPersonBalance; + private BigDecimal newAmount; + + private CurrencyConverter converter = new CurrencyConverterBuilder() + .strategy(Strategy.YAHOO_FINANCE_FILESTORE) + .buildConverter(); + + public CurrencyCommand(Index index, String fromCurrency, String toCurrency) { + this.index = index; + this.fromCurrency = fromCurrency; + this.toCurrency = toCurrency; + } + + @Override + public CommandResult execute() throws CommandException { + List lastShownList = model.getFilteredPersonList(); + converter.setRefreshRateSeconds(86400); + + if (index.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + convertedPerson = lastShownList.get(index.getZeroBased()); + convertedPersonBalance = convertedPerson.getMoney().balance; + + try { + newAmount = converter.convertCurrency(new BigDecimal(convertedPersonBalance), + Currency.get(fromCurrency), Currency.get(toCurrency)); + } catch (CurrencyNotSupportedException cnse) { + throw new AssertionError("Currency not supported"); + } catch (JSONException jsone) { + throw new AssertionError("JSON Exception"); + } catch (StorageException se) { + throw new AssertionError("Storage Exception"); + } catch (EndpointException ee) { + throw new AssertionError("Endpoint Exception"); + } catch (ServiceException se) { + throw new AssertionError("Service Exception"); + } catch (NullPointerException npe) { + throw new CommandException(MESSAGE_USAGE); + } + return new CommandResult(convertedPerson.getName() + "'s balance in " + toCurrency + " is: " + newAmount); + + } +} +``` diff --git a/collated/functional/software-1234.md b/collated/functional/software-1234.md new file mode 100644 index 000000000000..ac006718fb4c --- /dev/null +++ b/collated/functional/software-1234.md @@ -0,0 +1,258 @@ +# software-1234 +###### /java/seedu/address/logic/commands/CurrencyCommand.java +``` java +package seedu.address.logic.commands; + +import java.math.BigDecimal; +import java.util.List; + +import org.json.JSONException; + +import com.ritaja.xchangerate.api.CurrencyConverter; +import com.ritaja.xchangerate.api.CurrencyConverterBuilder; +import com.ritaja.xchangerate.api.CurrencyNotSupportedException; +import com.ritaja.xchangerate.endpoint.EndpointException; +import com.ritaja.xchangerate.service.ServiceException; +import com.ritaja.xchangerate.storage.StorageException; +import com.ritaja.xchangerate.util.Currency; +import com.ritaja.xchangerate.util.Strategy; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.person.Person; + +/** + * Lists all the commands entered by user from the start of app launch. + */ +public class CurrencyCommand extends Command { + + public static final String COMMAND_WORD = "convert"; + public static final String COMMAND_SHORTCUT = "cv"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Converts the balance of the person identified by the index number into a new " + + "currency chosen by the user. \n" + + "Parameters: INDEX (must be a positive integer) " + + "[Current Currency Symbol] " + + "[New Currency Symbol]\n" + + "Example: " + COMMAND_WORD + " 1" + " SGD" + " USD"; + public static final String MESSAGE_CURRENCY_NOT_SUPPORTED = "The currency you have provided is not supported."; + public static final String MESSAGE_SUCCESS = "Here is your balance in the new currency"; + private String fromCurrency; + private String toCurrency; + private Index index; + private Person convertedPerson; + private Double convertedPersonBalance = 0.0; + private BigDecimal newAmount; + + private CurrencyConverter converter = new CurrencyConverterBuilder() + .strategy(Strategy.YAHOO_FINANCE_FILESTORE) + .buildConverter(); + + public CurrencyCommand(Index index, String fromCurrency, String toCurrency) { + this.index = index; + this.fromCurrency = fromCurrency; + this.toCurrency = toCurrency; + } + + @Override + public CommandResult execute() throws CommandException { + List lastShownList = model.getFilteredPersonList(); + converter.setRefreshRateSeconds(86400); + + if (index.getZeroBased() > lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + if (index.getZeroBased() == 0) { + lastShownList = model.getFilteredPersonList(); + for (Person person : lastShownList) { + double currentPersonBalance = person.getMoney().balance; + convertedPersonBalance = convertedPersonBalance + currentPersonBalance; + } + } else { + convertedPerson = lastShownList.get(index.getZeroBased() - 1); + convertedPersonBalance = convertedPerson.getMoney().balance; + } + + try { + newAmount = converter.convertCurrency(new BigDecimal(convertedPersonBalance), + Currency.get(fromCurrency), Currency.get(toCurrency)); + } catch (CurrencyNotSupportedException cnse) { + throw new CommandException("Currency not supported"); + } catch (JSONException jsone) { + throw new AssertionError("JSON Exception"); + } catch (StorageException se) { + throw new AssertionError("Storage Exception"); + } catch (EndpointException ee) { + throw new AssertionError("Endpoint Exception"); + } catch (ServiceException se) { + throw new AssertionError("Service Exception"); + } catch (NullPointerException npe) { + throw new CommandException("Invalid currency"); + } + + if (index.getZeroBased() == 0) { + return new CommandResult("Your total balance in " + toCurrency + " is: " + newAmount); + } else { + return new CommandResult(convertedPerson.getName() + "'s balance in " + toCurrency + " is: " + newAmount); + } + + } +} +``` +###### /java/seedu/address/logic/commands/ListPositiveBalanceCommand.java +``` java +package seedu.address.logic.commands; + +import java.util.function.Predicate; + +import seedu.address.model.person.Person; + +/** + * Lists all persons with positive balances in the address book to the user. + */ +public class ListPositiveBalanceCommand extends Command { + + public static final String COMMAND_WORD = "lend"; + public static final String COMMAND_SHORTCUT = "le"; + + public static final String MESSAGE_SUCCESS = "Listed all persons who owe you money"; + + @Override + public CommandResult execute() { + + model.updateFilteredPersonList(isPositiveBalance()); + return new CommandResult(MESSAGE_SUCCESS); + } + + public Predicate isPositiveBalance() { + return a -> a.getMoney().balance > 0; + } +} +``` +###### /java/seedu/address/logic/commands/WipeBalancesCommand.java +``` java +package seedu.address.logic.commands; + +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.money.Money; +import seedu.address.model.person.Person; + +import seedu.address.model.person.exceptions.DuplicatePersonException; +import seedu.address.model.person.exceptions.PersonNotFoundException; + +/** + * Handles the balance command. + */ +public class WipeBalancesCommand extends UndoableCommand { + + public static final String COMMAND_WORD = "wipe"; + public static final String COMMAND_SHORTCUT = "w"; + + public static final String MESSAGE_SUCCESS = "Wiped all balances"; + public static final String MESSAGE_USAGE = COMMAND_WORD + "Wipes all balances"; + public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book."; + + private Money cleared = new Money("0.0"); + @Override + public CommandResult executeUndoableCommand() throws CommandException { + Person oldPerson; + try { + for (Person p : model.getFilteredPersonList()) { + oldPerson = p; + p.setMoney(cleared); + p.clearItems(); + model.updatePerson(oldPerson, p); + } + } catch (DuplicatePersonException dpe) { + throw new CommandException(MESSAGE_DUPLICATE_PERSON); + } catch (PersonNotFoundException pnfe) { + throw new AssertionError("The target person cannot be missing"); + } + return new CommandResult(MESSAGE_SUCCESS); + } + +} +``` +###### /java/seedu/address/logic/parser/CurrencyCommandParser.java +``` java +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import com.ritaja.xchangerate.util.Currency; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.logic.commands.CurrencyCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses user input. + */ +public class CurrencyCommandParser implements Parser { + + public static final String MESSAGE_INVALID_INDEX = "Index is not a non-zero unsigned integer."; + /** + * Parses the given {@code String} of arguments in the context of the CurrencyCommand + * and returns an CurrencyCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public CurrencyCommand parse(String args) throws ParseException { + + String fromCurrency; + String toCurrency; + Index currencyIndex; + try { + String trimmedArgs = args.trim(); + String[] currencyKeywords = trimmedArgs.split(" "); + if (Integer.parseInt(currencyKeywords[0]) == 0) { + currencyIndex = Index.fromZeroBased(0); + } else { + currencyIndex = Index.fromZeroBased(Integer.parseInt(currencyKeywords[0])); + } + + if (currencyIndex.getZeroBased() < 0) { + throw new IllegalValueException(MESSAGE_INVALID_INDEX); + } + fromCurrency = currencyKeywords[1].toUpperCase(); + toCurrency = currencyKeywords[2].toUpperCase(); + +``` +###### /java/seedu/address/model/money/Money.java +``` java + public static final String MONEY_VALIDATION_REGEX = "-?\\d+(\\.\\d+)?(E-?\\d+)?"; + + public final double balance; + public final String value; + + /** + * Constructs a {@code Money}. + * + * @param balance A valid money balance. + */ + public Money(String balance) { + requireNonNull(balance); + checkArgument(isValidMoney(balance), MESSAGE_MONEY_CONSTRAINTS); + this.balance = Double.parseDouble(balance); + this.value = balance; + } + + @Override + public String toString() { + return value; + } + + public Double toDouble() { + return balance; + } + + /** + * Returns true if a given string is a valid money balance. + */ + public static boolean isValidMoney(String test) { + return test.matches(MONEY_VALIDATION_REGEX); + } +``` diff --git a/collated/functional/sofware-1234.md b/collated/functional/sofware-1234.md new file mode 100644 index 000000000000..23736fb79e50 --- /dev/null +++ b/collated/functional/sofware-1234.md @@ -0,0 +1,30 @@ +# sofware-1234 +###### /java/seedu/address/logic/commands/ListNegativeBalanceCommand.java +``` java +package seedu.address.logic.commands; + +import java.util.function.Predicate; + +import seedu.address.model.person.Person; + +/** + * Lists all persons with negative balances in the address book to the user. + */ +public class ListNegativeBalanceCommand extends Command { + + public static final String COMMAND_WORD = "debt"; + public static final String COMMAND_SHORTCUT = "de"; + + public static final String MESSAGE_SUCCESS = "Listed all persons to which you owe money"; + + @Override + public CommandResult execute() { + + model.updateFilteredPersonList(isNegativeBalance()); + return new CommandResult(MESSAGE_SUCCESS); + } + public Predicate isNegativeBalance() { + return p -> p.getMoney().balance < 0; + } +} +``` diff --git a/collated/test/Articho28.md b/collated/test/Articho28.md new file mode 100644 index 000000000000..5194a0851610 --- /dev/null +++ b/collated/test/Articho28.md @@ -0,0 +1,431 @@ +# Articho28 +###### /java/seedu/address/logic/commands/BalanceCommandTest.java +``` java + +package seedu.address.logic.commands; + +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import org.junit.Before; +import org.junit.Test; + +import seedu.address.logic.CommandHistory; +import seedu.address.logic.UndoRedoStack; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +public class BalanceCommandTest { + + private Model model; + private Model expectedModel; + private BalanceCommand balanceCommand; + private double balance; + + + @Before + public void setUp() { + model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); + balanceCommand = new BalanceCommand(); + balanceCommand.setData(model, new CommandHistory(), new UndoRedoStack()); + balance = balanceCommand.getBalanceFromTravelBanker(); + } + + @Test + public void executes_getsOverallBalanceSuccess() { + assertCommandSuccess(balanceCommand, model, BalanceCommand.MESSAGE_SUCCESS + + "\n" + "Your balance is " + + BalanceCommand.getFormatTwoDecimalPlaces().format(balance) + + ".", expectedModel); + } + + + + +} +``` +###### /java/seedu/address/logic/commands/MapCommandTest.java +``` java + +package seedu.address.logic.commands; + +import static guitests.guihandles.WebViewUtil.waitUntilBrowserLoaded; +import static junit.framework.TestCase.assertEquals; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import java.net.MalformedURLException; +import java.net.URL; + +import org.junit.Before; +import org.junit.Test; + +import guitests.guihandles.BrowserPanelHandle; +import seedu.address.logic.CommandHistory; +import seedu.address.logic.UndoRedoStack; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.ui.BrowserPanel; +import seedu.address.ui.GuiUnitTest; + + + + +public class MapCommandTest extends GuiUnitTest { + + private BrowserPanel browserPanel; + private BrowserPanelHandle browserPanelHandle; + private MapCommand mapCommand; + private Model model; + private Model expectedModel; + + + @Before + public void setUp() { + model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); + + mapCommand = new MapCommand(); + mapCommand.setData(model, new CommandHistory(), new UndoRedoStack()); + + guiRobot.interact(() -> browserPanel = new BrowserPanel()); + uiPartRule.setUiPart(browserPanel); + + browserPanelHandle = new BrowserPanelHandle(browserPanel.getRoot()); + } + + @Test + public void executes_mapCommandRecognized() { + assertCommandSuccess(mapCommand, model, MapCommand.MESSAGE_SUCCESS, expectedModel); + } + + @Test public void executes_displaysMap() throws MalformedURLException { + CommandResult commandResult = mapCommand.execute(); + + waitUntilBrowserLoaded(browserPanelHandle); + assertEquals(new URL(BrowserPanel.ATM_SEARCH_PAGE_URL), browserPanelHandle.getLoadedUrl()); + } + + +} +``` +###### /java/seedu/address/logic/commands/MinCommandTest.java +``` java + +package seedu.address.logic.commands; + +import static junit.framework.TestCase.assertTrue; +import static org.testng.Assert.assertEquals; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.testutil.TypicalPersons.DANIEL; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import java.util.List; + +import org.junit.Before; +import org.junit.Test; + +import seedu.address.logic.CommandHistory; +import seedu.address.logic.UndoRedoStack; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.UserPrefs; +import seedu.address.model.money.Money; +import seedu.address.model.person.Person; +import seedu.address.model.person.exceptions.DuplicatePersonException; +import seedu.address.model.person.exceptions.PersonNotFoundException; + +/** + * Tests the output of the MinCommand + */ + +public class MinCommandTest { + + private Model expectedModelNoResultsFound; + private Model expectedModelResultsFound; + private int indexOfLowestBalance; + private MinCommand minCommandNoResultsFound; + private MinCommand minCommandResultsFound; + private Model model; + private Person actualPersonSelected; + private Person expectedPersonSelected; + + + @Before + public void setUp() { + expectedModelNoResultsFound = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + expectedModelResultsFound = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + + indexOfLowestBalance = getIndexOfPersonWithLowestBalance(model.getAddressBook()); + + minCommandNoResultsFound = new MinCommand(); + minCommandNoResultsFound.setData(model, new CommandHistory(), new UndoRedoStack()); + minCommandResultsFound = new MinCommand(); + minCommandResultsFound.setData(model, new CommandHistory(), new UndoRedoStack()); + + } + + /** + * Checks that it does nothing if all balances are positive. + */ + @Test + public void executeFindsNoResults() { + if (indexOfLowestBalance == -1) { + assertCommandSuccess(minCommandNoResultsFound, model, + MinCommand.MESSAGE_SUCCESS_NO_RESULT, expectedModelNoResultsFound); + } + } + + /** + * Finds the same person and checks if their balance is the same. + * + * @throws DuplicatePersonException + * @throws PersonNotFoundException + */ + @Test + public void executeFindsSamePerson() throws DuplicatePersonException, PersonNotFoundException { + insertNegativeBalance(model, DANIEL, new Money("-10")); + insertNegativeBalance(expectedModelResultsFound, DANIEL, new Money("-10")); + indexOfLowestBalance = getIndexOfPersonWithLowestBalance(expectedModelResultsFound.getAddressBook()); + expectedPersonSelected = expectedModelResultsFound.getFilteredPersonList().get(indexOfLowestBalance); + minCommandResultsFound.execute(); + actualPersonSelected = model.getFilteredPersonList().get(indexOfLowestBalance); + assertExecutionSuccess(expectedPersonSelected, actualPersonSelected); + assertTrue(actualPersonSelected.getMoney().value == expectedPersonSelected.getMoney().value); + } + + //@@ author + @Test + public void executes_updatedBalanceAccordingly() throws Exception { + model.getFilteredPersonList().get(0).setMoney(new Money("-100")); + + MinCommand minCommand = new MinCommand(); + minCommand.setData(model, new CommandHistory(), new UndoRedoStack()); + CommandResult commandResult = minCommand.execute(); + + assertEquals(commandResult.feedbackToUser, + MinCommand.MESSAGE_SUCCESS_FOUND + model.getFilteredPersonList().get(0).getName()); + } + +``` +###### /java/seedu/address/logic/commands/MinCommandTest.java +``` java + /** + * Gets the index number of the person with the lowest balance. Returns -1 if all balances are non-negative. + * + * @param addressBook + * @return + */ + public int getIndexOfPersonWithLowestBalance(ReadOnlyAddressBook addressBook) { + List persons = addressBook.getPersonList(); + int index = 0; + double lowestBalance = 0.0; + for (Person p : persons) { + double currentBalance = p.getMoney().balance; + if (currentBalance < lowestBalance) { + index = persons.indexOf(p); + lowestBalance = currentBalance; + } + } + if (lowestBalance == 0.0) { + index = -1; + return index; + } + return index; + } + + /** + * Inserts a person with a negative balance instead + * + * @param modelToChange + * @param toReplace + * @param money + * @throws PersonNotFoundException + * @throws DuplicatePersonException + */ + + public void insertNegativeBalance(Model modelToChange, Person toReplace, Money money) + throws PersonNotFoundException, DuplicatePersonException { + Person edited = new Person(toReplace.getName(), + toReplace.getPhone(), + toReplace.getEmail(), + toReplace.getAddress(), + money, + toReplace.getTags(), + toReplace.getItems()); + modelToChange.updatePerson(toReplace, edited); + } + + /** + * Checks if selected people are the same in both models. + */ + static void assertExecutionSuccess(Person expectedSelection, Person actualSelection) { + assertTrue(expectedSelection.equals(actualSelection)); + } +} +``` +###### /java/seedu/address/logic/commands/SearchTagCommandTest.java +``` java + +package seedu.address.logic.commands; + +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import java.util.HashSet; +import java.util.Set; + +import org.junit.Before; +import org.junit.Test; + +import seedu.address.logic.CommandHistory; +import seedu.address.logic.UndoRedoStack; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.tag.Tag; + +public class SearchTagCommandTest { + + private Model model; + private Model expectedModelSingleInput; + private Model expectedModelMultipleInput; + private SearchTagCommand searchTagCommandSingleTagInput; + private SearchTagCommand searchTagCommandMultipleTagsInput; + private Set multipleTagsAsInput; + private Set singleTagAsInput; + + + + @Before + public void setUp() { + + singleTagAsInput = new HashSet<>(); + singleTagAsInput.add(new Tag("friends")); + + multipleTagsAsInput = new HashSet<>(); + multipleTagsAsInput.add(new Tag("friends")); + multipleTagsAsInput.add(new Tag("classmates")); + multipleTagsAsInput.add(new Tag("colleagues")); + + model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + expectedModelSingleInput = new ModelManager(model.getAddressBook(), new UserPrefs()); + expectedModelSingleInput.updateFilteredPersonList(SearchTagCommand.personHasTags(singleTagAsInput)); + expectedModelMultipleInput = new ModelManager(model.getAddressBook(), new UserPrefs()); + expectedModelMultipleInput.updateFilteredPersonList(SearchTagCommand.personHasTags(multipleTagsAsInput)); + + searchTagCommandSingleTagInput = new SearchTagCommand(singleTagAsInput); + searchTagCommandMultipleTagsInput = new SearchTagCommand((multipleTagsAsInput)); + searchTagCommandSingleTagInput.setData(model, new CommandHistory(), new UndoRedoStack()); + searchTagCommandMultipleTagsInput.setData(model, new CommandHistory(), new UndoRedoStack()); + } + + @Test + public void showsAllContactWithFriendsTag() { + int result = expectedModelSingleInput.getFilteredPersonList().size(); + assertCommandSuccess(searchTagCommandSingleTagInput, model, SearchTagCommand.MESSAGE_SUCCESS + + "\n" + + SearchTagCommand.formatTagsFeedback(singleTagAsInput) + + "\n" + + Command.getMessageForPersonListShownSummary(result), expectedModelSingleInput); + } + + @Test + public void showsNoContactsWithMultipleTags() { + int result = expectedModelMultipleInput.getFilteredPersonList().size(); + assertCommandSuccess(searchTagCommandMultipleTagsInput, model, SearchTagCommand.MESSAGE_FAILURE + + "\n" + + SearchTagCommand.formatTagsFeedback(multipleTagsAsInput), expectedModelMultipleInput); + } + +} +``` +###### /java/seedu/address/logic/parser/SearchTagCommandParserTest.java +``` java + +package seedu.address.logic.parser; + +import static org.testng.Assert.assertEquals; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; + +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; + +import java.util.HashSet; +import java.util.Set; + +import org.junit.Test; + +import seedu.address.logic.commands.SearchTagCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.tag.Tag; + +/** + * Tests that the SeachTagCommandParser manipulates the input properly. + */ +public class SearchTagCommandParserTest { + + public static final String TAG_NAME_1 = "friends"; + public static final String TAG_NAME_2 = "colleagues"; + + private SearchTagCommandParser searchTagCommandParser = new SearchTagCommandParser(); + + /** + * Test for single input. + * @throws ParseException + */ + @Test + public void validArgsSingleTagInputIsEqual() throws ParseException { + Set tagsToFind = new HashSet<>(); + tagsToFind.add(new Tag(TAG_NAME_1)); + SearchTagCommand searchTagCommand = searchTagCommandParser.parse( " " + PREFIX_TAG + TAG_NAME_1); + assertEquals(searchTagCommand.getTagsToFind(), tagsToFind); + } + + /** + * Tests for multiple input. + * @throws ParseException + */ + @Test + public void validArgsMultipleTagsAreEqual() throws ParseException { + Set multipleTagsToFind = new HashSet<>(); + multipleTagsToFind.add(new Tag(TAG_NAME_1)); + multipleTagsToFind.add(new Tag(TAG_NAME_2)); + SearchTagCommand searchTagCommand = searchTagCommandParser.parse(" " + + PREFIX_TAG + TAG_NAME_1 + + " " + + PREFIX_TAG + TAG_NAME_2); + assertEquals(searchTagCommand.getTagsToFind(), multipleTagsToFind); + + } + /** + * Throws exception when no tags are provided. + */ + @Test + public void emptyTagsArgs() { + assertParseFailure(searchTagCommandParser, "a", + String.format(SearchTagCommandParser.MESSAGE_INVALID_COMMAND_NO_TAGS, SearchTagCommand.MESSAGE_USAGE)); + } +} +``` +###### /java/seedu/address/ui/BrowserPanelTest.java +``` java + @Test + public void display() throws Exception { + // default web page + URL expectedDefaultPageUrl = new URL(BrowserPanel.ATM_SEARCH_PAGE_URL); + assertEquals(expectedDefaultPageUrl, browserPanelHandle.getLoadedUrl()); + + // associated web page of a person + postNow(selectionChangedEventStub); + URL expectedPersonUrl = new URL(BrowserPanel.ADDRESS_SEARCH_PAGE_URL + + BOB.getAddress().value.replaceAll(" ", "%20")); + + waitUntilBrowserLoaded(browserPanelHandle); + assertEquals(expectedPersonUrl, browserPanelHandle.getLoadedUrl()); + } +} +``` diff --git a/collated/test/chenchongsong.md b/collated/test/chenchongsong.md new file mode 100644 index 000000000000..be22dc203991 --- /dev/null +++ b/collated/test/chenchongsong.md @@ -0,0 +1,667 @@ +# chenchongsong +###### /java/seedu/address/logic/commands/CommandTestUtil.java +``` java + public static final String VALID_BILL = " " + PREFIX_MONEY + "100.00"; + public static final String INVALID_BILL = " " + PREFIX_MONEY + "100k"; +``` +###### /java/seedu/address/logic/commands/RemoveTagCommandTest.java +``` 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_TAG_FRIEND; +import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_HUSBAND; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandFailure; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.logic.commands.CommandTestUtil.prepareRedoCommand; +import static seedu.address.logic.commands.CommandTestUtil.prepareUndoCommand; +import static seedu.address.logic.commands.CommandTestUtil.showPersonAtIndex; +import static seedu.address.logic.commands.RemoveTagCommand.createEditedPerson; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; +import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND_PERSON; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import java.util.HashSet; + +import org.junit.Test; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.CommandHistory; +import seedu.address.logic.UndoRedoStack; +import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; +import seedu.address.model.AddressBook; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.person.Person; +import seedu.address.testutil.EditPersonDescriptorBuilder; +import seedu.address.testutil.PersonBuilder; + +/** + * Contains integration test (interaction with the Model, UndoCommand and RedoCommand) and unit tests for EditCommand. + */ +public class RemoveTagCommandTest { + + private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + + @Test + public void execute_allFieldsSpecifiedUnfilteredList_success() throws Exception { + Person personToEdit = model.getFilteredPersonList().get(0); + EditPersonDescriptor descriptor = new EditPersonDescriptorBuilder(personToEdit).build(); + RemoveTagCommand removeTagCommand = prepareCommand(INDEX_FIRST_PERSON, descriptor); + + Person tagRemovedPerson = new Person( + personToEdit.getName(), + personToEdit.getPhone(), + personToEdit.getEmail(), + personToEdit.getAddress(), + personToEdit.getMoney(), + new HashSet<>() + ); + String expectedMessage = String.format(RemoveTagCommand.MESSAGE_REMOVE_TAG_SUCCESS, tagRemovedPerson); + + Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), new UserPrefs()); + expectedModel.updatePerson(personToEdit, tagRemovedPerson); + + assertCommandSuccess(removeTagCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_someFieldsSpecifiedUnfilteredList_success() throws Exception { + Index indexLastPerson = Index.fromOneBased(model.getFilteredPersonList().size()); + Person lastPerson = model.getFilteredPersonList().get(indexLastPerson.getZeroBased()); + // lastPerson contains original info + + PersonBuilder personInList = new PersonBuilder(lastPerson); + Person tagRemovedPerson = personInList + .withName(lastPerson.getName().toString()) + .withPhone(lastPerson.getPhone().toString()) + .withEmail(lastPerson.getEmail().toString()) + .withAddress(lastPerson.getAddress().toString()) + .withMoney(lastPerson.getMoney().toString()) + .withoutTags().build(); + + EditPersonDescriptor descriptor = new EditPersonDescriptorBuilder() + .withTags(lastPerson.getTags()).build(); + RemoveTagCommand removeTagCommand = prepareCommand(indexLastPerson, descriptor); + + String expectedMessage = String.format(RemoveTagCommand.MESSAGE_REMOVE_TAG_SUCCESS, tagRemovedPerson); + + Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), new UserPrefs()); + expectedModel.updatePerson(lastPerson, tagRemovedPerson); + + assertCommandSuccess(removeTagCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_invalidPersonIndexUnfilteredList_failure() { + Index outOfBoundIndex = Index.fromOneBased(model.getFilteredPersonList().size() + 1); + EditPersonDescriptor descriptor = new EditPersonDescriptorBuilder() + .withTags(VALID_TAG_HUSBAND).build(); + RemoveTagCommand removeTagCommand = prepareCommand(outOfBoundIndex, descriptor); + + assertCommandFailure(removeTagCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + /** + * Remove Tags from filtered list where index is larger than size of filtered list, + * but smaller than size of address book + */ + @Test + public void execute_invalidPersonIndexFilteredList_failure() { + showPersonAtIndex(model, INDEX_FIRST_PERSON); + Index outOfBoundIndex = INDEX_SECOND_PERSON; + // ensures that outOfBoundIndex is still in bounds of address book list + assertTrue(outOfBoundIndex.getZeroBased() < model.getAddressBook().getPersonList().size()); + + RemoveTagCommand removeTagCommand = prepareCommand(outOfBoundIndex, + new EditPersonDescriptorBuilder().withTags(VALID_TAG_FRIEND).build()); + + assertCommandFailure(removeTagCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + @Test + public void executeUndoRedo_validIndexUnfilteredList_success() throws Exception { + UndoRedoStack undoRedoStack = new UndoRedoStack(); + UndoCommand undoCommand = prepareUndoCommand(model, undoRedoStack); + RedoCommand redoCommand = prepareRedoCommand(model, undoRedoStack); + + Person personToEdit = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); + + EditPersonDescriptor descriptor = new EditPersonDescriptorBuilder(personToEdit).build(); + // descriptor contains all tags to be removed + + Person tagRemovedPerson = createEditedPerson(personToEdit, descriptor); + + RemoveTagCommand removeTagCommand = prepareCommand(INDEX_FIRST_PERSON, descriptor); + Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), new UserPrefs()); + + // remove tags -> first person tags removed + removeTagCommand.execute(); + undoRedoStack.push(removeTagCommand); + + // undo -> reverts addressbook back to previous state and filtered person list to show all persons + assertCommandSuccess(undoCommand, model, UndoCommand.MESSAGE_SUCCESS, expectedModel); + + // redo -> same first person's tag removed again + expectedModel.updatePerson(personToEdit, tagRemovedPerson); + assertCommandSuccess(redoCommand, model, RedoCommand.MESSAGE_SUCCESS, expectedModel); + } + + @Test + public void equals() throws Exception { + Person personToEdit = model.getFilteredPersonList().get(0); + EditPersonDescriptor descriptor = new EditPersonDescriptorBuilder(personToEdit).build(); + + final RemoveTagCommand standardCommand = prepareCommand(INDEX_FIRST_PERSON, descriptor); + + // same values -> returns true + EditPersonDescriptor copyDescriptor = new EditPersonDescriptor(descriptor); + RemoveTagCommand commandWithSameValues = prepareCommand(INDEX_FIRST_PERSON, copyDescriptor); + assertTrue(standardCommand.equals(commandWithSameValues)); + + // same object -> returns true + assertTrue(standardCommand.equals(standardCommand)); + + // one command preprocessed when previously equal -> returns false + commandWithSameValues.preprocessUndoableCommand(); + assertFalse(standardCommand.equals(commandWithSameValues)); + + // null -> returns false + assertFalse(standardCommand.equals(null)); + + // different types -> returns false + assertFalse(standardCommand.equals(new ClearCommand())); + + // different index -> returns false + assertFalse(standardCommand.equals(new RemoveTagCommand(INDEX_SECOND_PERSON, DESC_AMY))); + + // different descriptor -> returns false + assertFalse(standardCommand.equals(new RemoveTagCommand(INDEX_FIRST_PERSON, DESC_BOB))); + } + + /** + * Returns an {@code EditCommand} with parameters {@code index} and {@code descriptor} + */ + private RemoveTagCommand prepareCommand(Index index, EditPersonDescriptor descriptor) { + RemoveTagCommand removeTagCommand = new RemoveTagCommand(index, descriptor); + removeTagCommand.setData(model, new CommandHistory(), new UndoRedoStack()); + return removeTagCommand; + } + +} +``` +###### /java/seedu/address/logic/commands/SplitCommandTest.java +``` java +package seedu.address.logic.commands; + +import static org.junit.Assert.assertEquals; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; +import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND_PERSON; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import java.util.ArrayList; + +import org.junit.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.CommandHistory; +import seedu.address.logic.UndoRedoStack; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; + +public class SplitCommandTest { + + private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + + @Test + public void executes_splitBalance() throws Exception { + ArrayList indices = new ArrayList<>(); + indices.add(INDEX_FIRST_PERSON); + indices.add(INDEX_SECOND_PERSON); + + double expectedBalance1 = model.getFilteredPersonList() + .get(INDEX_FIRST_PERSON.getZeroBased()).getMoney().toDouble() + 50.0; + double expectedBalance2 = model.getFilteredPersonList() + .get(INDEX_SECOND_PERSON.getZeroBased()).getMoney().toDouble() + 50.0; + + SplitCommand splitCommand = new SplitCommand(indices, 100.0); + splitCommand.setData(model, new CommandHistory(), new UndoRedoStack()); + splitCommand.execute(); + + assertEquals(expectedBalance1, + model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()).getMoney().toDouble(), 0.001); + + assertEquals(expectedBalance2, + model.getFilteredPersonList().get(INDEX_SECOND_PERSON.getZeroBased()).getMoney().toDouble(), 0.001); + } +} +``` +###### /java/seedu/address/logic/parser/ItemAddCommandParserTest.java +``` java +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_MONEY; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; + +import org.junit.Test; + +import seedu.address.logic.commands.ItemAddCommand; + +public class ItemAddCommandParserTest { + + public static final String VALID_ITEM_NAME = "Taxi Fare"; + public static final String VALID_ITEM_VALUE = "10.23"; + public static final String INVALID_ITEM_NAME = "Taxi*&(Fare)"; + public static final String INVALID_ITEM_VALUE = "10k"; + + + private ItemAddCommandParser parser = new ItemAddCommandParser(); + + @Test + public void parse_validArgs_returnsItemAddCommand() { + assertParseSuccess(parser, "1 " + PREFIX_NAME + VALID_ITEM_NAME + " " + PREFIX_MONEY + VALID_ITEM_VALUE, + new ItemAddCommand(INDEX_FIRST_PERSON, VALID_ITEM_NAME, VALID_ITEM_VALUE)); + } + + @Test + public void parse_invalidArgs_throwsParseException() { + + // 0 as targetIndex + assertParseFailure(parser, "0 " + PREFIX_NAME + VALID_ITEM_NAME + " " + PREFIX_MONEY + VALID_ITEM_VALUE, + String.format(MESSAGE_INVALID_COMMAND_FORMAT, ItemAddCommand.MESSAGE_USAGE)); + + // no prefix "n/" + assertParseFailure(parser, "1 " + VALID_ITEM_NAME + " " + PREFIX_MONEY + VALID_ITEM_VALUE, + String.format(MESSAGE_INVALID_COMMAND_FORMAT, ItemAddCommand.MESSAGE_USAGE)); + + // no prefix "m/" + assertParseFailure(parser, "1 " + PREFIX_NAME + VALID_ITEM_NAME + " " + VALID_ITEM_VALUE, + String.format(MESSAGE_INVALID_COMMAND_FORMAT, ItemAddCommand.MESSAGE_USAGE)); + + // invalid item name + assertParseFailure(parser, "1 " + PREFIX_NAME + INVALID_ITEM_NAME + " " + PREFIX_MONEY + VALID_ITEM_VALUE, + ItemAddCommand.MESSAGE_INVALID_ARGUMENT); + + // invalid item value + assertParseFailure(parser, "1 " + PREFIX_NAME + VALID_ITEM_NAME + " " + PREFIX_MONEY + INVALID_ITEM_VALUE, + ItemAddCommand.MESSAGE_INVALID_ARGUMENT); + } +} +``` +###### /java/seedu/address/logic/parser/ItemDeleteCommandParserTest.java +``` 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 static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_ITEM; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; + +import org.junit.Test; + +import seedu.address.logic.commands.ItemDeleteCommand; + +public class ItemDeleteCommandParserTest { + + private ItemDeleteCommandParser parser = new ItemDeleteCommandParser(); + + @Test + public void parse_validArgs_returnsItemDeleteCommand() { + assertParseSuccess(parser, "1 1", + new ItemDeleteCommand(INDEX_FIRST_PERSON, INDEX_FIRST_ITEM)); + } + + @Test + public void parse_invalidArgs_throwsParseException() { + assertParseFailure(parser, "a", + String.format(MESSAGE_INVALID_COMMAND_FORMAT, ItemDeleteCommand.MESSAGE_USAGE)); + assertParseFailure(parser, "1 1 1", + String.format(MESSAGE_INVALID_COMMAND_FORMAT, ItemDeleteCommand.MESSAGE_USAGE)); + assertParseFailure(parser, "0 1", + String.format(MESSAGE_INVALID_COMMAND_FORMAT, ItemDeleteCommand.MESSAGE_USAGE)); + assertParseFailure(parser, "1 0", + String.format(MESSAGE_INVALID_COMMAND_FORMAT, ItemDeleteCommand.MESSAGE_USAGE)); + } +} +``` +###### /java/seedu/address/logic/parser/ItemShowCommandParserTest.java +``` 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 static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; + +import org.junit.Test; + +import seedu.address.logic.commands.ItemShowCommand; + +/** + * Test scope: similar to {@code DeleteCommandParserTest}. + * @see DeleteCommandParserTest + */ +public class ItemShowCommandParserTest { + + private ItemShowCommandParser parser = new ItemShowCommandParser(); + + @Test + public void parse_validArgs_returnsItemShowCommand() { + assertParseSuccess(parser, "1", new ItemShowCommand(INDEX_FIRST_PERSON)); + } + + @Test + public void parse_invalidArgs_throwsParseException() { + assertParseFailure(parser, "a", String.format(MESSAGE_INVALID_COMMAND_FORMAT, ItemShowCommand.MESSAGE_USAGE)); + } +} +``` +###### /java/seedu/address/logic/parser/RemoveTagCommandParserTest.java +``` java +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; + +import java.util.HashSet; +import java.util.Set; + +import org.junit.Test; + +import seedu.address.logic.commands.EditCommand; +import seedu.address.logic.commands.RemoveTagCommand; +import seedu.address.model.tag.Tag; + +public class RemoveTagCommandParserTest { + + public static final String TAG_NAME_1 = "friends"; + public static final String TAG_NAME_2 = "owesMoney"; + + private RemoveTagCommandParser parser = new RemoveTagCommandParser(); + + @Test + public void parse_validArgsSingleTag_returnsRemoveTagCommand() { + Set tagsToRemoved = new HashSet<>(); + tagsToRemoved.add(new Tag(TAG_NAME_1)); + + EditCommand.EditPersonDescriptor editPersonDescriptor = new EditCommand.EditPersonDescriptor(); + editPersonDescriptor.setTags(tagsToRemoved); + + assertParseSuccess(parser, "1 " + PREFIX_TAG + TAG_NAME_1, + new RemoveTagCommand(INDEX_FIRST_PERSON, editPersonDescriptor)); + } + + @Test + public void parse_validArgsMultipleTags_returnsRemoveTagCommand() { + Set tagsToRemoved = new HashSet<>(); + tagsToRemoved.add(new Tag(TAG_NAME_1)); + tagsToRemoved.add(new Tag(TAG_NAME_2)); + + EditCommand.EditPersonDescriptor editPersonDescriptor = new EditCommand.EditPersonDescriptor(); + editPersonDescriptor.setTags(tagsToRemoved); + + assertParseSuccess(parser, "1 " + PREFIX_TAG + TAG_NAME_1 + " " + PREFIX_TAG + TAG_NAME_2, + new RemoveTagCommand(INDEX_FIRST_PERSON, editPersonDescriptor)); + } + + @Test + public void parse_invalidArgs_throwsParseException() { + // no tags + assertParseFailure(parser, "1", + String.format(MESSAGE_INVALID_COMMAND_FORMAT, RemoveTagCommand.MESSAGE_USAGE)); + + // no index + assertParseFailure(parser, "t/friends", + String.format(MESSAGE_INVALID_COMMAND_FORMAT, RemoveTagCommand.MESSAGE_USAGE)); + + // 0 index + assertParseFailure(parser, "0 t/friends", + String.format(MESSAGE_INVALID_COMMAND_FORMAT, RemoveTagCommand.MESSAGE_USAGE)); + } +} +``` +###### /java/seedu/address/logic/parser/SortCommandParserTest.java +``` java +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_MONEY; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; + +import org.junit.Test; + +import seedu.address.logic.commands.SortCommand; + +public class SortCommandParserTest { + + + private SortCommandParser parser = new SortCommandParser(); + + @Test + public void parse_validArgs_returnsSortCommand() { + + assertParseSuccess(parser, PREFIX_ADDRESS + SortCommand.SORT_ORDER_ASCENDING, + new SortCommand(PREFIX_ADDRESS.toString(), SortCommand.SORT_ORDER_ASCENDING)); + + assertParseSuccess(parser, PREFIX_PHONE + SortCommand.SORT_ORDER_DESCENDING, + new SortCommand(PREFIX_PHONE.toString(), SortCommand.SORT_ORDER_DESCENDING)); + + assertParseSuccess(parser, PREFIX_EMAIL + SortCommand.SORT_ORDER_ASCENDING, + new SortCommand(PREFIX_EMAIL.toString(), SortCommand.SORT_ORDER_ASCENDING)); + + assertParseSuccess(parser, PREFIX_NAME + SortCommand.SORT_ORDER_DESCENDING, + new SortCommand(PREFIX_NAME.toString(), SortCommand.SORT_ORDER_DESCENDING)); + + assertParseSuccess(parser, PREFIX_MONEY + SortCommand.SORT_ORDER_ASCENDING, + new SortCommand(PREFIX_MONEY.toString(), SortCommand.SORT_ORDER_ASCENDING)); + + assertParseSuccess(parser, PREFIX_TAG + SortCommand.SORT_ORDER_DESCENDING, + new SortCommand(PREFIX_TAG.toString(), SortCommand.SORT_ORDER_DESCENDING)); + + } + + @Test + public void parse_invalidArgs_throwsParseException() { + // duplicate + assertParseFailure(parser, "n/asc e/asc", + String.format(MESSAGE_INVALID_COMMAND_FORMAT, SortCommand.MESSAGE_USAGE)); + + // typo + assertParseFailure(parser, "e/ascc", + String.format(MESSAGE_INVALID_COMMAND_FORMAT, SortCommand.MESSAGE_USAGE)); + + // typo + assertParseFailure(parser, "tt/desc", + String.format(MESSAGE_INVALID_COMMAND_FORMAT, SortCommand.MESSAGE_USAGE)); + } +} +``` +###### /java/seedu/address/logic/parser/SplitCommandParserTest.java +``` java +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_MONEY; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; +import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND_PERSON; +import static seedu.address.testutil.TypicalIndexes.INDEX_THIRD_PERSON; + +import java.util.ArrayList; + +import org.junit.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.SplitCommand; + +public class SplitCommandParserTest { + + private static final String VALID_INDEX = "1"; + private static final String VALID_INDICES_1 = "1 2 3"; + private static final String VALID_INDICES_2 = "1 1 1 2"; + private static final String INVALID_INDEX_1 = "a"; + private static final String INVALID_INDEX_2 = "0"; + private static final String VALID_BILL_1 = PREFIX_MONEY + "100.00"; + private static final String VALID_BILL_2 = PREFIX_MONEY + "0.12"; + + private SplitCommandParser parser = new SplitCommandParser(); + + @Test + public void parse_validArgsSingleIndex_returnsSplitCommand() { + ArrayList indices = new ArrayList<>(); + indices.add(INDEX_FIRST_PERSON); + assertParseSuccess(parser, VALID_INDEX + " " + VALID_BILL_1, + new SplitCommand(indices, 100.00)); + } + + @Test + public void parse_validArgsMultipleIndex_returnsSplitCommand() { + ArrayList indices = new ArrayList<>(); + indices.add(INDEX_FIRST_PERSON); + indices.add(INDEX_SECOND_PERSON); + indices.add(INDEX_THIRD_PERSON); + assertParseSuccess(parser, VALID_INDICES_1 + " " + VALID_BILL_2, + new SplitCommand(indices, 0.12)); + + // In this case, the first person would take 3/4 of that bill + // and the second person would take 1/4 of that bill + indices = new ArrayList<>(); + indices.add(INDEX_FIRST_PERSON); + indices.add(INDEX_FIRST_PERSON); + indices.add(INDEX_FIRST_PERSON); + indices.add(INDEX_SECOND_PERSON); + assertParseSuccess(parser, VALID_INDICES_2 + " " + VALID_BILL_2, + new SplitCommand(indices, 0.12)); + } + + @Test + public void parse_invalidArgs_throwsParseException() { + assertParseFailure(parser, INVALID_INDEX_1 + " " + VALID_BILL_1, + String.format(MESSAGE_INVALID_COMMAND_FORMAT, SplitCommand.MESSAGE_USAGE)); + + assertParseFailure(parser, INVALID_INDEX_2 + " " + VALID_BILL_2, + String.format(MESSAGE_INVALID_COMMAND_FORMAT, SplitCommand.MESSAGE_USAGE)); + } + +} +``` +###### /java/seedu/address/model/item/ItemTest.java +``` java +package seedu.address.model.item; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import seedu.address.testutil.Assert; + +public class ItemTest { + + private static final String VALID_ITEM_NAME_1 = "Taxi Fare Split"; + private static final String VALID_ITEM_NAME_2 = "Taxi_Fare_Split"; + private static final String VALID_ITEM_NAME_3 = "Taxi Fare Split"; + private static final String VALID_ITEM_NAME_4 = "Taxi__Fare__Split"; + private static final String INVALID_ITEM_NAME = ""; + private static final String INVALID_ITEM_VALUE_1 = ""; + + @Test + public void constructor_null_throwsNullPointerException() { + Assert.assertThrows(NullPointerException.class, () -> new Item(null, null)); + } + + @Test + public void constructor_invalidItemName_throwsIllegalArgumentException() { + Assert.assertThrows(IllegalArgumentException.class, () -> new Item(INVALID_ITEM_NAME, INVALID_ITEM_VALUE_1)); + } + + @Test + public void isValidItemName() throws Exception { + // null name + Assert.assertThrows(NullPointerException.class, () -> Item.isValidItemName(null)); + + // invalid name + assertFalse(Item.isValidItemName("")); // empty string + assertFalse(Item.isValidItemName("^")); // only non-alphanumeric characters + assertFalse(Item.isValidItemName("item*")); // contains non-alphanumeric characters + + // valid name + assertTrue(Item.isValidItemName("someitemname")); // alphabets only + assertTrue(Item.isValidItemName("12345")); // numbers only + assertTrue(Item.isValidItemName("some item name 123456")); // alphanumeric characters + assertTrue(Item.isValidItemName("Some Item Name")); // with capital letters + assertTrue(Item.isValidItemName(VALID_ITEM_NAME_1)); // separated by space + assertTrue(Item.isValidItemName(VALID_ITEM_NAME_2)); // separated by _ + assertTrue(Item.isValidItemName(VALID_ITEM_NAME_3)); // separated by two spaces + assertTrue(Item.isValidItemName(VALID_ITEM_NAME_4)); // separated by __ + } + + @Test + public void isValidItemValue() throws Exception { + // null name + Assert.assertThrows(NullPointerException.class, () -> Item.isValidItemValue(null)); + + // valid item value + assertTrue(Item.isValidItemValue("0")); // numbers only + assertTrue(Item.isValidItemValue("123456")); // multiple digits + assertTrue(Item.isValidItemValue("10.2345")); // with multiple decimal places + assertTrue(Item.isValidItemValue("12345678978978987978987987987987")); // long digits + assertTrue(Item.isValidItemValue("123456.123E8")); // scientific representation of floating point numbers + + // invalid item value + assertFalse(Item.isValidItemValue("")); + assertFalse(Item.isValidItemValue("10.")); + assertFalse(Item.isValidItemValue("123E8E6")); + assertFalse(Item.isValidItemValue("10k")); + } + +} +``` +###### /java/seedu/address/testutil/EditPersonDescriptorBuilder.java +``` java + /** + * Copy a Tag Set {@code toCopy} and set the copy into the {@code EditPersonDescriptor} + * that we are building. + */ + public EditPersonDescriptorBuilder withTags(Set toCopy) { + descriptor.setTags(toCopy); + return this; + } +``` +###### /java/seedu/address/testutil/PersonBuilder.java +``` java + /** + * Set an empty set {@code Set} and set it to the {@code Person} that we are building. + */ + public PersonBuilder withoutTags() { + this.tags = SampleDataUtil.getTagSet(); + return this; + } +``` +###### /java/seedu/address/testutil/TypicalIndexes.java +``` java + public static final Index INDEX_FIRST_ITEM = Index.fromOneBased(1); + public static final Index INDEX_SECOND_ITEM = Index.fromOneBased(2); + public static final Index INDEX_THIRD_ITEM = Index.fromOneBased(3); +``` diff --git a/collated/test/pkuhanan.md b/collated/test/pkuhanan.md new file mode 100644 index 000000000000..ae4bca7a6104 --- /dev/null +++ b/collated/test/pkuhanan.md @@ -0,0 +1,39 @@ +# pkuhanan +###### /java/seedu/address/commons/util/XmlUtilTest.java +``` java + private static final String VALID_BALANCE = "10"; +``` +###### /java/seedu/address/logic/commands/CommandTestUtil.java +``` java + public static final String VALID_MONEY_AMY = "10"; + public static final String VALID_MONEY_BOB = "10"; +``` +###### /java/seedu/address/logic/commands/CommandTestUtil.java +``` java + public static final String MONEY_DESC_AMY = " " + PREFIX_MONEY + VALID_MONEY_AMY; + public static final String MONEY_DESC_BOB = " " + PREFIX_MONEY + VALID_MONEY_BOB; +``` +###### /java/seedu/address/storage/XmlAdaptedPersonTest.java +``` java + private static final String VALID_BALANCE = BENSON.getMoney().toString(); +``` +###### /java/seedu/address/testutil/EditPersonDescriptorBuilder.java +``` java + /** + * Sets the {@code Money} of the {@code EditPersonDescriptor} that we are building. + */ + public EditPersonDescriptorBuilder withMoney(String money) { + descriptor.setMoney(new Money(money)); + return this; + } +``` +###### /java/seedu/address/testutil/PersonBuilder.java +``` java + /** + * Sets the {@code Money} of the {@code Person} that we are building. + */ + public PersonBuilder withMoney(String balance) { + this.balance = new Money(balance); + return this; + } +``` diff --git a/collated/test/software-1234.md b/collated/test/software-1234.md new file mode 100644 index 000000000000..a5b0bc8f3a74 --- /dev/null +++ b/collated/test/software-1234.md @@ -0,0 +1,98 @@ +# software-1234 +###### /java/seedu/address/logic/commands/ListBalancesCommandTest.java +``` java + +package seedu.address.logic.commands; + +import static org.junit.Assert.assertEquals; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import org.junit.Test; + +import seedu.address.logic.CommandHistory; +import seedu.address.logic.UndoRedoStack; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.money.Money; + +public class ListBalancesCommandTest { + private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + + @Test + public void executes_updatesFilteredListAccordingly() throws Exception { + model.getFilteredPersonList().get(0).setMoney(new Money("100")); + + ListPositiveBalanceCommand listPositiveBalanceCommand = new ListPositiveBalanceCommand(); + listPositiveBalanceCommand.setData(model, new CommandHistory(), new UndoRedoStack()); + listPositiveBalanceCommand.execute(); + + for (int i = 0; i < model.getAddressBook().getPersonList().size(); i++) { + assertEquals(model.getAddressBook().getPersonList().get(i), model.getFilteredPersonList().get(i)); + } + + model.getAddressBook().getPersonList().get(0).setMoney(new Money("-100")); + model.getAddressBook().getPersonList().get(1).setMoney(new Money("-150")); + model.getAddressBook().getPersonList().get(3).setMoney(new Money("-200")); + + ListNegativeBalanceCommand listNegativeBalanceCommand = new ListNegativeBalanceCommand(); + listNegativeBalanceCommand.setData(model, new CommandHistory(), new UndoRedoStack()); + listNegativeBalanceCommand.execute(); + + assertEquals(model.getAddressBook().getPersonList().get(0), model.getFilteredPersonList().get(0)); + assertEquals(model.getAddressBook().getPersonList().get(1), model.getFilteredPersonList().get(1)); + assertEquals(model.getAddressBook().getPersonList().get(3), model.getFilteredPersonList().get(2)); + + } + + @Test + public void noPositiveOrNegativeAmounts() throws Exception { + + ListNegativeBalanceCommand listNegativeBalanceCommand = new ListNegativeBalanceCommand(); + listNegativeBalanceCommand.setData(model, new CommandHistory(), new UndoRedoStack()); + listNegativeBalanceCommand.execute(); + + assertEquals(0, model.getFilteredPersonList().size()); + + for (int i = 0; i < model.getAddressBook().getPersonList().size(); i++) { + model.getAddressBook().getPersonList().get(i).setMoney(new Money("-100")); + } + + ListPositiveBalanceCommand listPositiveBalanceCommand = new ListPositiveBalanceCommand(); + listPositiveBalanceCommand.setData(model, new CommandHistory(), new UndoRedoStack()); + listPositiveBalanceCommand.execute(); + + assertEquals(0, model.getFilteredPersonList().size()); + + } +} +``` +###### /java/seedu/address/logic/commands/WipeCommandTest.java +``` java +public class WipeCommandTest { + private Model model; + private Model expectedModel; + private WipeBalancesCommand wipeBalancesCommand; + private double balance; + + + @Before + public void setUp() throws CommandException { + model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); + wipeBalancesCommand = new WipeBalancesCommand(); + + wipeBalancesCommand.setData(model, new CommandHistory(), new UndoRedoStack()); + wipeBalancesCommand.execute(); + + } + + @Test + public void executes_getsWipeSuccess() { + for (int i = 0; i < model.getFilteredPersonList().size(); i++) { + assertEquals(0.0, model.getFilteredPersonList().get(i).getMoney().balance, 0.001); + } + + } +} +``` diff --git a/convert_image.png b/convert_image.png new file mode 100644 index 000000000000..2d8e20661a49 Binary files /dev/null and b/convert_image.png differ diff --git a/docs/AboutUs.adoc b/docs/AboutUs.adoc index 0f0a8e7ab51e..8af906ecd0db 100644 --- a/docs/AboutUs.adoc +++ b/docs/AboutUs.adoc @@ -3,53 +3,42 @@ :imagesDir: images :stylesDir: stylesheets -AddressBook - Level 4 was developed by the https://se-edu.github.io/docs/Team.html[se-edu] team. + -_{The dummy content given below serves as a placeholder to be used by future forks of the project.}_ + -{empty} + -We are a team based in the http://www.comp.nus.edu.sg[School of Computing, National University of Singapore]. +TravelBanker - v1.5 was developed by CS2103 Team T11-B4 during their study at the National University of Singapore. == Project Team -=== John Doe -image::damithc.jpg[width="150", align="left"] -{empty}[http://www.comp.nus.edu.sg/~damithch[homepage]] [https://github.com/damithc[github]] [<>] +=== Artsiom Skillar +image::Articho28.jpg[width="150", align="left"] +{empty}[https://github.com/Articho28[github]] [<>] -Role: Project Advisor +Role: Team Lead + Deliverables and Deadlines ''' -=== John Roe -image::lejolly.jpg[width="150", align="left"] -{empty}[http://github.com/lejolly[github]] [<>] +=== Prian Kudahan +image::pkunahan.jpg[width="150", align="left"] +{empty}[http://github.com/pkuhanan[github]] [<>] -Role: Team Lead + -Responsibilities: UI +Role: Testing -''' - -=== Johnny Doe -image::yijinl.jpg[width="150", align="left"] -{empty}[http://github.com/yijinl[github]] [<>] - -Role: Developer + -Responsibilities: Data ''' -=== Johnny Roe -image::m133225.jpg[width="150", align="left"] -{empty}[http://github.com/m133225[github]] [<>] +=== Chen Chongsong +image::chenchongsong.jpg[width="150", align="left"] +{empty}[http://github.com/chenchongsong[github]] [<>] + +Role: Code Quality -Role: Developer + -Responsibilities: Dev Ops + Threading ''' -=== Benson Meier -image::yl_coder.jpg[width="150", align="left"] -{empty}[http://github.com/yl-coder[github]] [<>] +=== Eric Zhou +image::software-1234.jpg[width="150", align="left"] +{empty}[http://github.com/software-1234[github]] [<>] + +Role: Documentation -Role: Developer + -Responsibilities: UI ''' + diff --git a/docs/ContactUs.adoc b/docs/ContactUs.adoc index eafdc9574a50..4587b9234e44 100644 --- a/docs/ContactUs.adoc +++ b/docs/ContactUs.adoc @@ -1,6 +1,6 @@ = Contact Us :stylesDir: stylesheets -* *Bug reports, Suggestions* : Post in our https://github.com/se-edu/addressbook-level4/issues[issue tracker] if you noticed bugs or have suggestions on how to improve. +* *Bug reports, Suggestions* : Post in our https://github.com/CS2103JAN2018-T11-B4/main/issues[issue tracker] if you noticed bugs or have suggestions on how to improve. * *Contributing* : We welcome pull requests. Follow the process described https://github.com/oss-generic/process[here] * *Email us* : You can also reach us at `damith [at] comp.nus.edu.sg` diff --git a/docs/DeveloperGuide.adoc b/docs/DeveloperGuide.adoc index 1733af113b29..b780f9192a8c 100644 --- a/docs/DeveloperGuide.adoc +++ b/docs/DeveloperGuide.adoc @@ -1,4 +1,4 @@ -= AddressBook Level 4 - Developer Guide += TravelBanker - Developer Guide :toc: :toc-title: :toc-placement: preamble @@ -10,9 +10,9 @@ ifdef::env-github[] :tip-caption: :bulb: :note-caption: :information_source: endif::[] -:repoURL: https://github.com/se-edu/addressbook-level4/tree/master +:repoURL: https://github.com/se-edu/TravelBanker-level4/tree/master -By: `Team SE-EDU`      Since: `Jun 2016`      Licence: `MIT` +By: `CS2103JAN18-T11-B4`      Since: `Jun 2016`      Licence: `MIT` == Setting up @@ -68,7 +68,7 @@ Optionally, you can follow the <> docume ==== Updating documentation to match your fork -After forking the repo, links in the documentation will still point to the `se-edu/addressbook-level4` repo. If you plan to develop this as a separate product (i.e. instead of contributing to the `se-edu/addressbook-level4`) , you should replace the URL in the variable `repoURL` in `DeveloperGuide.adoc` and `UserGuide.adoc` with the URL of your fork. +After forking the repo, links in the documentation will still point to the `CS2103JAN2018/main` repo. If you plan to develop this as a separate product (i.e. instead of contributing to the `CS2103JAN2018/main`) , you should replace the URL in the variable `repoURL` in `DeveloperGuide.adoc` and `UserGuide.adoc` with the URL of your fork. ==== Setting up CI @@ -140,7 +140,7 @@ The _Sequence Diagram_ below shows how the components interact for the scenario image::SDforDeletePerson.png[width="800"] [NOTE] -Note how the `Model` simply raises a `AddressBookChangedEvent` when the Address Book data are changed, instead of asking the `Storage` to save the updates to the hard disk. +Note how the `Model` simply raises a `TravelBankerChangedEvent` when the accounting book data are changed, instead of asking the `Storage` to save the updates to the hard 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. @@ -183,7 +183,7 @@ image::LogicCommandClassDiagram.png[width="800"] *API* : link:{repoURL}/src/main/java/seedu/address/logic/Logic.java[`Logic.java`] -. `Logic` uses the `AddressBookParser` class to parse the user command. +. `Logic` uses the `TravelBankerParser` class to parse the user command. . This results in a `Command` object which is executed by the `LogicManager`. . The command execution can affect the `Model` (e.g. adding a person) and/or raise events. . The result of the command execution is encapsulated as a `CommandResult` object which is passed back to the `Ui`. @@ -204,7 +204,7 @@ image::ModelClassDiagram.png[width="800"] The `Model`, * stores a `UserPref` object that represents the user's preferences. -* stores the Address Book data. +* stores the accounting book data. * exposes an unmodifiable `ObservableList` that can be 'observed' e.g. the UI can be bound to this list so that the UI automatically updates when the data in the list change. * does not depend on any of the other three components. @@ -219,28 +219,142 @@ image::StorageClassDiagram.png[width="800"] The `Storage` component, * can save `UserPref` objects in json format and read it back. -* can save the Address Book data in xml format and read it back. +* can save the accounting book data in xml format and read it back. [[Design-Commons]] === Common classes -Classes used by multiple components are in the `seedu.addressbook.commons` package. +Classes used by multiple components are in the `seedu.TravelBanker.commons` package. == Implementation This section describes some noteworthy details on how certain features are implemented. -// tag::undoredo[] +//tag::convert[] +=== Currency Conversion feature +==== Current Implementation + +The currency conversion feature is implemented using a currencyConverter repo that was found from github. While the converter itself was resued code, the implementation in the app was done entirely on my own. The converter itself supports quick conversion between two currencies for individual people and total balances. This was neccesary because it is not guaranteed that the people will always be using the same currency + +In order to allow the currency command to work, I had to create a new parser for currency. The parser `CurrencyCommandParser` parses through the command to check for the index and to/from currency codes. If the index is 0, then we know that the whole balance needs to be converted. + +Then those parameters are passed to the currency command class where the constructor is as follows: + +[source,java] +---- +public CurrencyCommand(Index index, String fromCurrency, String toCurrency) { + this.index = index; + this.fromCurrency = fromCurrency; + this.toCurrency = toCurrency; +} +---- + +For the new `CurrencyCommand` class, the execute code is: + +[source,java] +---- +public CommandResult execute() throws CommandException { + List lastShownList = model.getFilteredPersonList(); + converter.setRefreshRateSeconds(86400); + + if (index.getZeroBased() > lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + if (index.getZeroBased() == 0) { + lastShownList = model.getFilteredPersonList(); + for (Person person : lastShownList) { + double currentPersonBalance = person.getMoney().balance; + convertedPersonBalance = convertedPersonBalance + currentPersonBalance; + } + } else { + convertedPerson = lastShownList.get(index.getZeroBased() - 1); + convertedPersonBalance = convertedPerson.getMoney().balance; + } + + try { + newAmount = converter.convertCurrency(new BigDecimal(convertedPersonBalance), + Currency.get(fromCurrency), Currency.get(toCurrency)); + } catch (CurrencyNotSupportedException cnse) { + throw new CommandException("Currency not supported"); + } catch (JSONException jsone) { + throw new AssertionError("JSON Exception"); + } catch (StorageException se) { + throw new AssertionError("Storage Exception"); + } catch (EndpointException ee) { + throw new AssertionError("Endpoint Exception"); + } catch (ServiceException se) { + throw new AssertionError("Service Exception"); + } catch (NullPointerException npe) { + throw new CommandException("Invalid currency"); + } + + if (index.getZeroBased() == 0) { + return new CommandResult("Your total balance in " + toCurrency + " is: " + newAmount); + } else { + return new CommandResult(convertedPerson.getName() + "'s balance in " + toCurrency + " is: " + newAmount); + } +---- + +As we see, the parameters are passed in, if the index is 0, then the balances are added up or else the balance of the individual person is found. Then in the try block, the conversion takes place and then the applicable message is returned. + +==== Future Additions + +I could further refine the currency converter to allow an amount to be converted into multiple currencies and then you can choose the currency you want. Furthermore, we could add a currency field to the whole addressbook allowing for total changes of the currency of the addressbook allowing users to select their desired currency and allow for more convienience with the addressbook. + +//end::convert[] +// tag::posneg[] +=== List Positive/Negative feature +==== Current Implementation + +The positive/negative feature is faciliatated by two new methods `ListPositiveBalanceCommand` and `ListNegativeBalanceCommand` which both reside inside `commands`. It supports listing out the people with negative balances and positive balances. This is helpful because it will allow the phonebook user to quickly list out people who they owe and who owes them money. + +For the new class I created `ListPositiveBalanceCommand`, this is the code for execute: + +[source,java] +---- +public CommandResult execute() { + model.updateFilteredPersonList(isPositiveBalance()); + return new CommandResult(MESSAGE_SUCCESS); +} +---- + +As you can see from the code snippet, we update the filtered person list using a new function `isPositiveBalance()`. + +`isPositiveBalance()` is implemented as follows: + +[source,java] +---- +public Predicate isPositiveBalance(){ + return a -> a.getMoney().balance >= 0; +} +---- + +The negative balance works the same way just checking to see if the balance is less than 0. + +==== Alternatives Considered + +For an alternative way, I considered creating a new empty list `positiveList` and then adding to `positiveList` whenever the balance is greater than 0. Then I was going to display positive list. However this did not end up working because I noticed that updating the flitered list needed a predicate function as a paramter which my approach would not have. Therefore I had to consider a different approach that involved a predicate function. + +==== Future Additions + +I can further refine the function to be much more powerful in filtering and listing out people. For instance, I could allow it to list people with whose names start with a specific letter because in an large addressbook, the user might want to quickly go through the addressbook, not ones who just have a postiive or negative balance. + + +The function is a predicate function that checks to see if `a.getMoney().balance>=0` because the function `updateFilteredPersonList()` takes in a predicate parameter. In our function we see the predicate check to see if the person's money balance is positive and if so, it will be returned. + +//end::posneg[] +//tag::undoredo[] === Undo/Redo feature ==== Current Implementation -The undo/redo mechanism is facilitated by an `UndoRedoStack`, which resides inside `LogicManager`. It supports undoing and redoing of commands that modifies the state of the address book (e.g. `add`, `edit`). Such commands will inherit from `UndoableCommand`. +The undo/redo mechanism is facilitated by an `UndoRedoStack`, which resides inside `LogicManager`. It supports undoing and redoing of commands that modifies the state of the accounting book (e.g. `add`, `edit`). Such commands will inherit from `UndoableCommand`. `UndoRedoStack` only deals with `UndoableCommands`. Commands that cannot be undone will inherit from `Command` instead. The following diagram shows the inheritance diagram for commands: image::LogicCommandClassDiagram.png[width="800"] -As you can see from the diagram, `UndoableCommand` adds an extra layer between the abstract `Command` class and concrete commands that can be undone, such as the `DeleteCommand`. Note that extra tasks need to be done when executing a command in an _undoable_ way, such as saving the state of the address book before execution. `UndoableCommand` contains the high-level algorithm for those extra tasks while the child classes implements the details of how to execute the specific command. Note that this technique of putting the high-level algorithm in the parent class and lower-level steps of the algorithm in child classes is also known as the https://www.tutorialspoint.com/design_pattern/template_pattern.htm[template pattern]. +As you can see from the diagram, `UndoableCommand` adds an extra layer between the abstract `Command` class and concrete commands that can be undone, such as the `DeleteCommand`. Note that extra tasks need to be done when executing a command in an _undoable_ way, such as saving the state of the accounting book before execution. `UndoableCommand` contains the high-level algorithm for those extra tasks while the child classes implements the details of how to execute the specific command. Note that this technique of putting the high-level algorithm in the parent class and lower-level steps of the algorithm in child classes is also known as the https://www.tutorialspoint.com/design_pattern/template_pattern.htm[template pattern]. Commands that are not undoable are implemented this way: [source,java] @@ -275,7 +389,7 @@ public class DeleteCommand extends UndoableCommand { Suppose that the user has just launched the application. The `UndoRedoStack` will be empty at the beginning. -The user executes a new `UndoableCommand`, `delete 5`, to delete the 5th person in the address book. The current state of the address book is saved before the `delete 5` command executes. The `delete 5` command will then be pushed onto the `undoStack` (the current state is saved together with the command). +The user executes a new `UndoableCommand`, `delete 5`, to delete the 5th person in the accounting book. The current state of the accounting book is saved before the `delete 5` command executes. The `delete 5` command will then be pushed onto the `undoStack` (the current state is saved together with the command). image::UndoRedoStartingStackDiagram.png[width="800"] @@ -288,7 +402,7 @@ If a command fails its execution, it will not be pushed to the `UndoRedoStack` a The user now decides that adding the person was a mistake, and decides to undo that action using `undo`. -We will pop the most recent command out of the `undoStack` and push it back to the `redoStack`. We will restore the address book to the state before the `add` command executed. +We will pop the most recent command out of the `undoStack` and push it back to the `redoStack`. We will restore the accounting book to the state before the `add` command executed. image::UndoRedoExecuteUndoStackDiagram.png[width="800"] @@ -299,7 +413,7 @@ The following sequence diagram shows how the undo operation works: image::UndoRedoSequenceDiagram.png[width="800"] -The redo does the exact opposite (pops from `redoStack`, push to `undoStack`, and restores the address book to the state after the command is executed). +The redo does the exact opposite (pops from `redoStack`, push to `undoStack`, and restores the accounting book to the state after the command is executed). [NOTE] If the `redoStack` is empty, then there are no other commands left to be redone, and an `Exception` will be thrown when popping the `redoStack`. @@ -329,7 +443,7 @@ image::UndoRedoActivityDiagram.png[width="650"] ===== Aspect: How undo & redo executes -* **Alternative 1 (current choice):** Saves the entire address book. +* **Alternative 1 (current choice):** Saves the entire accounting 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. @@ -339,13 +453,13 @@ image::UndoRedoActivityDiagram.png[width="650"] ===== Aspect: Type of commands that can be undone/redone -* **Alternative 1 (current choice):** Only include commands that modifies the address book (`add`, `clear`, `edit`). +* **Alternative 1 (current choice):** Only include commands that modifies the accounting book (`add`, `clear`, `edit`). ** Pros: We only revert changes that are hard to change back (the view can easily be re-modified as no data are * lost). ** Cons: User might think that undo also applies when the list is modified (undoing filtering for example), * only to realize that it does not do that, after executing `undo`. * **Alternative 2:** Include all commands. ** Pros: Might be more intuitive for the user. ** Cons: User have no way of skipping such commands if he or she just want to reset the state of the address * book and not the view. -**Additional Info:** See our discussion https://github.com/se-edu/addressbook-level4/issues/390#issuecomment-298936672[here]. +**Additional Info:** See our discussion https://github.com/se-edu/TravelBanker-level4/issues/390#issuecomment-298936672[here]. ===== Aspect: Data structure to support the undo/redo commands @@ -358,6 +472,199 @@ image::UndoRedoActivityDiagram.png[width="650"] ** 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::balancefeature[] +=== Balance Feature +The balance feature is implemented by the `BalanceCommand` class, which resides in the `Logic` component. +It extends the Command class and is not an undoable or re-doable command, similar to `list`. The balance command relies on +the incorporation of the [MONEY] field, which stores the amount that a specific contact owes to the user, +or the amount the user owes to the said contact if the balance is negative. + +The following class diagram shows shows where where the BalanceCommand is implemented. + +image::BalanceCommand - Class Diagram.png[width="800"] + +Commands that cannot be undone such as BalanceCommand are implemented like this: + +[source,java] +---- +public class ListCommand extends Command { + @Override + public CommandResult execute() { + // ... list logic ... + } +} +---- + +The following sequence diagram shows how the `balance` command works. + +image::BalanceCommandSequenceDiagram.png[width="800"] + +Suppose that the user just launched the application. The TravelBanker will load his contacts and the amount of money owed by/to each +will be shown to the screen. The user simply types `balance` or `b`, which will be interpreted by the TravelBook Parser. +Once the command is parsed, it will return a new `BalanceCommand`, which `LogicManager` will call `command`. `LogicManager` will +then call the execute() function on command. This method calls on `getBalancefromTravelBanker` gets a `Persons` list through `model`, +and thus gets to manipulate the values in the `m/[MONEY]` field of the current accounting book. It then adds all of them +and displays to the feedback to the user by returning a `CommandResult` with the found data as argument. + +The following activity diagram summarizes the execution of the `balance` command. + +image::BalanceCommandActivityDiagram.png[width="800"] + +==== Design Considerations + +===== Aspect: Making balance not an undoable or re-doable command. + +* **Alternative 1 (current choice)**: Make `balance` not an undoable command**. + +* **Pros**: There was no need to make balance a re-doable or undoable command since it serves a purpose similar to `list`: +it only displays information found in TravelBanker, but does not alter it. Thus, the user will not have any issues with this +command should he make a mistake: it does not write any data. + +* ** Cons**: No significant disadvantage. + +* **Alternative 2: make `balance` an undoable command.** +* **Pros**: No serious advantage, as explained in Alternative 1. Since the command does not aim to modify data, +this method is not applicable. +* **Cons**: Extra layer of complexity that does not give any significant benefit + +===== Aspect: How balance command result is displayed. +* **Alternative 1 (current choice)** : Pass the result as a string in the feedback to user. +* **Pros** : Easy to implement and serves the purpose perfectly. +* **Cons** : Value cannot be singled out to apply UI effects, such as colors. + +* **Alternative 2 ** : Single out result to make it modifiable by UI. +* **Pros**: Allow for more pleasant user experience. +* **Cons**: More difficult to implement and not much value added. +// end::balancefeature[] + +// tag::itemfield[] +=== Add/Delete/Show Item Field + +==== Current Implementation + +The implementation of the item field touches three components: Model, Logic, and Storage. + +===== Model + +For Model component, `Person` class was modified and added with exactly one `UniqueItemList`, where each `UniqueItemList` consists of multiple `Item`s. For each `Item`, it has two strings as private attributes, namely the `ItemName` () + +A partial class diagram of the models can be seen below: + +image::itemDiagram.png[width="800"] + +The `isValidName` and `isValidValue` methods are used to make sure that the user input conforms to the regex for a name and a floating point number: + +===== Logic + +In the Logic component, three new command, `ItemShowCommand` `ItemAddCommand` `ItemDeleteCommand`, have been added. +`ItemAddCommand` and `ItemDeleteCommand` are undoable, because these two commands are implemented by replacing the old person in the `ModelManager` with a newly modified person. + +[source,java] +---- + model.updatePerson(personToEdit, editedPerson); +---- + +[NOTE] +In ItemAddCommand, `editedPerson` is created from personToEdit with a newly added item. +In ItemDeleteCommand, however, `editedPerson` is created from personToEdit by deleting a specific item. + +===== Storage + +Storage was also changed in the development of this feature, as new XML elements had to be stored and parsed using the xml storage system. + +The three types of new XML elements are `` `` and ``, and they are organised as following: + +[source,xml] +---- + + John Doe + 98765432 + ...... + + taxi fare + 10.5 + + + his treat in PizzaHut + 23.0 + + +---- + +To conform to the required changes, the `XmlAdaptedPerson` class is modified. Additionally, a new class `XmlAdaptedItem` is created . + +The new `XmlAdaptedPerson` class is as follows: + +[source,java] +---- +public class XmlAdaptedPerson { + @XmlElement(required = true) + private String name; + @XmlElement(required = true) + private String phone; + @XmlElement(required = true) + private String email; + @XmlElement(required = true) + private String address; + @XmlElement + private String balance; + @XmlElement + private List tagged = new ArrayList<>(); + + + @XmlElement + private List items = new ArrayList<>(); + + ...... +} +---- + +Here is the new `XmlAdaptedItem` class: + +[source,java] +---- +public class XmlAdaptedItem { + + @XmlElement(required = true) + private String name; + @XmlElement(required = true) + private String value; + + ...... +} +---- + +// end::itemfield[] + +// tag::sortfeature[] +=== sort Feature +The sort feature is implemented by the `sortCommand` class, which resides in the `Logic` component. +When `sortCommand` is executed, it would call the `Model` component (`UniquePersonList`) to sort the person list. To specify the sorting order and the keyword to be sorted, two strings would be passed as parameters. +In support of different keyword, Class `Name`, `Phone`, `Email`, `Address`, `Money` were add with a `compareTo` method for the creation of comparators. + +The sortCommand supports sorting of the filteredList (i.e. list that comes from FindCommand execution). + +In the current implementation, the sorted result would not be store in the storage. + +Commands that cannot be undone such as sortCommand are implemented like this: + +[source,java] +---- +public class sortCommand extends Command { + @Override + public CommandResult execute() { + // ... sort logic ... + return new CommandResult(MESSAGE_SUCCESS); + } +} +---- + +The following activity diagram summarizes the execution of the `sort` command. + +image::SortCommand_Activity_Diagram.png[width="800"] + +// end::sortfeature[] + // tag::dataencryption[] === [Proposed] Data Encryption @@ -365,6 +672,120 @@ _{Explain here how the data encryption feature will be implemented}_ // end::dataencryption[] +// tag::moneyfield[] +=== Add/Edit Money Field + +==== Current Implementation + +The implementation of the money field spanned four components: Model, Logic, Storage and UI. + +===== Model + +In terms of the Model component, a new Money model was created and the Person and Addressbook models required modifications to integrate the new Money model. The Money model was written to be consistent with the existing Person attribute models like "Email" and "Phone". + +A class diagram of the models can be seen below: + +image::MoneyClassDiagram.png[width="800"] + +The most important part of the Money model is the constructor: + +[source,java] +---- +public Money(String balance) { + requireNonNull(balance); + checkArgument(isValidMoney(balance), MESSAGE_MONEY_CONSTRAINTS); + this.balance = Double.parseDouble(balance); + this.value = balance; +} +---- + +The constructor was implemented using two properties: `Double balance` and `String value` +This is because the money value is often used as a string for display and as a double for calculations and comparisons. +The trade-off here was to either store it as just a Double or a String, and cast the value into the right type when needed, or store the value as both a Double and a String. +The first option forgoes time performance to provide better space performance and keep one single source of truth for each Money object. The second option however provides better time performance, but falls short on space and source of truth data integrity. +Our team decided to take the second option as data integrity issues can be mitigated through comprehensive testing, and because time performance is more important than space complexity at this point time. + +The other notable parts of the Money model are the validation checking function `isValidMoney` and the `equals` function. + +The `isValidMoney` function is used to make sure that the user input conforms to the regex for a number: + +[source,java] +---- +public static boolean isValidMoney(String test) { + return test.matches(MONEY_VALIDATION_REGEX); +} +---- + +The `equals` function is mostly used for testing, and provides a way to check if two Money objects have equal values. +The function makes sure that the objects are of the same type, and share the same `value` property: + +[source,java] +---- +public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Money // instanceof handles nulls + && this.value.equals(((Money) other).value)); // state check +} +---- + +There were also small modifications to the AddressBook and Person models. These changes mainly have to do with adding Money as a property of Person, and making sure the getters and setters work appropriately for that property. + +===== Logic + +In the context of the Logic component, the "add" and "edit" components had to be modified. In addition, some parser logic had to be modified in order to work with the new money parameter. +To be more specific, one of the major changes here was the `parseMoney` function which is used to parse the input from the user into a Money object: + +[source,java] +---- +public static Money parseMoney(String money) throws IllegalValueException { + requireNonNull(money); + String trimmedMoney = money.trim(); + if (!Money.isValidMoney(trimmedMoney)) { + throw new IllegalValueException(Money.MESSAGE_MONEY_CONSTRAINTS); + } + return new Money(trimmedMoney); +} +---- + +[NOTE] +In this implmentation, if an invalid input is received for the money parameter, which is anything that isn't a Double, an IllegalValueExcpetion is thrown. + +This implementation was chosen as to stay consistent with the rest of the existing fields. In other words, this `parseMoney` function is comparable to the `parseEmail` or `parsePhone` functions, in order to maintain consistency in parser logic. + +===== Storage + +Storage was also changed in the development of this feature, as a new parameter had to be stored and parsed using the xml storage system. + +The logic for parsing the stored data is very similar to the parsing logic for user input: + +[source,java] +---- +if (!Money.isValidMoney(this.balance)) { + throw new IllegalValueException(Money.MESSAGE_MONEY_CONSTRAINTS); +} +final Money balance = new Money(this.balance); +---- + +Then this money object is used to create the Person object: + +[source,java] +---- +return new Person(name, phone, email, address, balance, tags); +---- + +Again, this implementation was chosen to be consistent with the existing logic of parsing the stored xml. + +===== UI + +The changes in the UI were minor, and simply added the money field to the `PersonCard` UI component: + +[source,fxml] +---- +