diff --git a/.gitignore b/.gitignore index 823d175eb670..533bffbd7be4 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,5 @@ classes/ /bin/ src/main/resources/docs/ out/ +Collate.bat +Collate-TUI.jar diff --git a/README.adoc b/README.adoc index 03eff3a4d191..2f77808fcf76 100644 --- a/README.adoc +++ b/README.adoc @@ -1,11 +1,9 @@ -= Address Book (Level 4) += CoinBook 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://gitter.im/se-edu/Lobby[image:https://badges.gitter.im/se-edu/Lobby.svg[Gitter chat]] +https://travis-ci.org/CS2103JAN2018-F09-B3/main[image:https://travis-ci.org/CS2103JAN2018-F09-B3/main.svg?branch=master[Build Status]] +https://ci.appveyor.com/project/ewaldhew/main[image:https://ci.appveyor.com/api/projects/status/anm4ynat6657reac?svg=true[Build Status]] +https://coveralls.io/github/CS2103JAN2018-F09-B3/main?branch=master[image:https://coveralls.io/repos/github/CS2103JAN2018-F09-B3/main/badge.svg?branch=master&service=github[Coverage Status]] ifdef::env-github[] image::docs/images/Ui.png[width="600"] @@ -15,26 +13,41 @@ ifndef::env-github[] image::images/Ui.png[width="600"] endif::[] -* This is a desktop Address Book application. It has a GUI but most of the user interactions happen using a CLI (Command Line Interface). -* It is a Java sample application intended for students learning Software Engineering while using Java as the main programming language. -* It is *written in OOP fashion*. It provides a *reasonably well-written* code example that is *significantly bigger* (around 6 KLoC)than what students usually write in beginner-level SE modules. -* What's different from https://github.com/se-edu/addressbook-level3[level 3]: -** A more sophisticated GUI that includes a list panel and an in-built Browser. -** More test cases, including automated GUI testing. -** Support for _Build Automation_ using Gradle and for _Continuous Integration_ using Travis CI. +** This is a desktop Cryptocurrency management app for storing and keeping track of one’s cryptocurrency portfolio + +** Has a GUI but most of the user interactions happen using CLI (Command Line Interface) + +** Java application targeted at enthusiasts or investors who trade heavily and actively in cryptocurrencies + +** Provides various statistics such as Relative Strength Index (RSI) and moving average convergence divergence (MACD) charts to aid users with their decision making + +** User won't have to check news and statistics on another platform; CoinBook offers an integrated solution + +** Price history allows user to see price changes over time + +** Newsfeed for user to keep up to date on relevant news and happenings == Site Map +ifdef::env-github[] +* link:https://cs2103jan2018-f09-b3.github.io/main/UserGuide.html[User Guide] +* link:https://cs2103jan2018-f09-b3.github.io/main/DeveloperGuide.html[Developer Guide] +* link:https://cs2103jan2018-f09-b3.github.io/main/AboutUs.html[About Us] +* link:https://cs2103jan2018-f09-b3.github.io/main/ContactUs.html[Contact Us] +endif::[] + +ifndef::env-github[] * <> * <> -* <> * <> * <> +endif::[] == Acknowledgements +* Based off the AddressBook-Level4 project, a software project for students learning Software Engineering with Java as the main programming language. Created by the SE-EDU initiative at https://github.com/se-edu/. * 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] +* 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], http://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs/overview-summary.html[Apache HttpClient], https://github.com/google/gson/[GSON], https://github.com/AsyncHttpClient/async-http-client[AsyncHttpClient] == Licence : link:LICENSE[MIT] diff --git a/appveyor.yml b/appveyor.yml index 4660f313ab8b..030250272ba5 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -5,7 +5,7 @@ # --no-daemon: Prevent the daemon from launching to prevent file-in-use errors # when we cache the ~/.gradle directory build_script: - - gradlew.bat --no-daemon assemble checkstyleMain checkstyleTest + - gradlew.bat --no-daemon fixWhitespace assemble checkstyleMain checkstyleTest test_script: - appveyor-retry gradlew.bat --no-daemon headless allTests diff --git a/build.gradle b/build.gradle index 50cd2ae52efc..a260622f94ee 100644 --- a/build.gradle +++ b/build.gradle @@ -26,6 +26,30 @@ repositories { maven { url "https://oss.sonatype.org/content/repositories/snapshots/" } } +task makeTempFiles(type: Copy) { + from 'docs' + into 'temp' + include '**/*.adoc' + include '**/*.css' +} + +task fixWhitespace(type: Copy) { + from 'temp' + into 'docs' + include '**/*.adoc' + include '**/*.css' + filter { String line -> + line.replaceAll(/ +$/, '') + } +} + +task deleteTempFiles(type: Delete) { + delete 'temp' +} + +fixWhitespace.dependsOn makeTempFiles +fixWhitespace.finalizedBy deleteTempFiles + checkstyle { toolVersion = '8.1' } @@ -46,6 +70,9 @@ 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 group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5.1' + compile group: 'com.google.code.gson', name: 'gson', version: '2.8.2' + compile group: 'org.asynchttpclient', name: 'async-http-client', version: '2.4.4' testCompile group: 'junit', name: 'junit', version: '4.12' testCompile group: 'org.testfx', name: 'testfx-core', version: testFxVersion @@ -57,7 +84,7 @@ dependencies { } shadowJar { - archiveName = "addressbook.jar" + archiveName = "coinbook.jar" destinationDir = file("${buildDir}/jar/") } @@ -72,7 +99,7 @@ task coverage(type: JacocoReport) { executionData = files(allprojects.jacocoTestReport.executionData) afterEvaluate { classDirectories = files(classDirectories.files.collect { - fileTree(dir: it, exclude: ['**/*.jar']) + fileTree(dir: it, exclude: ['**/*.jar', '**/*.png']) }) } reports { diff --git a/collated/functional/Eldon-Chung.md b/collated/functional/Eldon-Chung.md new file mode 100644 index 000000000000..88e04d5a89a4 --- /dev/null +++ b/collated/functional/Eldon-Chung.md @@ -0,0 +1,979 @@ +# Eldon-Chung +###### \java\seedu\address\commons\core\CoinSubredditList.java +``` java +//Citation included here because .json files cannot be commented on. +//the list of subreddits CoinCodeToSubreddit.json was partially +//obtained from https://github.com/kendricktan/cryptoshitposting/blob/master/data/subreddits.json +public class CoinSubredditList { + + private static final Map COIN_CODE_TO_SUBREDDIT_MAP = new HashMap<>(); + private static final String COIN_CODE_TO_SUBREDDIT_FILEPATH = "/coins/CoinCodeToSubreddit.json"; + private static final String REDDIT_URL = "https://www.reddit.com/r/"; + private static final String CODE_ATTRIBUTE_NAME = "code"; + private static final String SUBREDDIT_ATTRIBUTE_NAME = "subreddit"; + + public static boolean isRecognized(Coin coin) { + return COIN_CODE_TO_SUBREDDIT_MAP.containsKey(coin.getCode().toString()); + } + + /** + * Obtains the subreddit url associated with the {@code coin}. + * @param coin + * @return the subreddit url associated with the {@code coin} + */ + public static String getRedditUrl(Coin coin) { + return REDDIT_URL + COIN_CODE_TO_SUBREDDIT_MAP.get(coin.getCode().toString()); + } + + /** + * Initialises the CoinToSubredditList to store existing Coin subreddits + * @throws FileNotFoundException if the file missing + */ + public static void initialize() throws FileNotFoundException, URISyntaxException { + InputStreamReader fileReader = new InputStreamReader(getCoinCodeToSubredditFilepath()); + JsonArray jsonArray = parseFileToJsonObj(fileReader); + JsonElement codeString; + JsonElement subredditName; + + for (JsonElement jsonElement : jsonArray) { + JsonObject jsonObject = jsonElement.getAsJsonObject(); + codeString = jsonObject.get(CODE_ATTRIBUTE_NAME); + subredditName = jsonObject.get(SUBREDDIT_ATTRIBUTE_NAME); + if (subredditName == null || subredditName.toString().equals("null")) { + continue; + } + COIN_CODE_TO_SUBREDDIT_MAP.put(codeString.getAsString(), subredditName.getAsString()); + } + } + + private static InputStream getCoinCodeToSubredditFilepath() throws URISyntaxException { + return MainApp.class.getResourceAsStream(COIN_CODE_TO_SUBREDDIT_FILEPATH); + } +} +``` +###### \java\seedu\address\logic\commands\FindCommand.java +``` java + public FindCommand(String description, Predicate coinCondition) { + this.description = description; + this.coinCondition = coinCondition; + } + + @Override + public boolean equals(Object other) { + /* + * Note: there isn't a good way to evaluate equality. + * There are ways around it, but it is not clear whether those drastic measures are needed. + * So we will always return false instead. + */ + return false; + } + + @Override + public CommandResult execute() { + model.updateFilteredCoinList(coinCondition); + EventsCenter.getInstance().post(new FilterChangedEvent(description)); + return new CommandResult(getMessageForCoinListShownSummary(model.getFilteredCoinList().size())); + } +``` +###### \java\seedu\address\logic\conditions\AmountCondition.java +``` java +/** + * Represents the predicates that evaluate two Amount objects. Is + */ +public abstract class AmountCondition implements Predicate { + + protected BiPredicate amountComparator; + protected Amount amount; + + public AmountCondition(Amount amount, BiPredicate amountComparator) { + this.amount = amount; + this.amountComparator = amountComparator; + } + + public abstract boolean test(Coin coin); +} +``` +###### \java\seedu\address\logic\conditions\AmountHeldCondition.java +``` java +/** + * Represents a predicate that evaluates to true when the amount held of a {@Coin} is either greater than or less than + * (depending on the amount comparator) the amount specified. + */ +public class AmountHeldCondition extends AmountCondition { + + public static final TokenType PREFIX = PREFIX_HELD; + public static final TokenType PARAMETER_TYPE = NUM; + + public AmountHeldCondition(Amount amount, BiPredicate amountComparator) { + super(amount, amountComparator); + } + + @Override + public boolean test(Coin coin) { + return amountComparator.test(coin.getCurrentAmountHeld(), amount); + } +} +``` +###### \java\seedu\address\logic\conditions\CodeCondition.java +``` java +/** + * Represents a predicate that evaluates to true when a {@Coin} contains the {@Code} specified. + */ +public class CodeCondition implements Predicate { + + public static final TokenType PREFIX = PREFIX_CODE; + public static final TokenType PARAMETER_TYPE = STRING; + + private String substring; + + public CodeCondition(String substring) { + this.substring = substring; + } + + @Override + public boolean test(Coin coin) { + return coin.getCode().toString().toUpperCase().contains(substring.toUpperCase()); + } +} +``` +###### \java\seedu\address\logic\conditions\CurrentPriceCondition.java +``` java +/** + * Represents a predicate that evaluates to true when the price of a {@Coin} is either greater than or less than + * (depending on the amount comparator) the amount specified. + */ +public class CurrentPriceCondition extends AmountCondition { + + public static final TokenType PREFIX = PREFIX_PRICE; + public static final TokenType PARAMETER_TYPE = NUM; + + + public CurrentPriceCondition(Amount amount, BiPredicate amountComparator) { + super(amount, amountComparator); + } + + @Override + public boolean test(Coin coin) { + return amountComparator.test(new Amount(coin.getPrice().getCurrent()), amount); + } +} +``` +###### \java\seedu\address\logic\conditions\DollarsBoughtCondition.java +``` java +/** + * Represents a predicate that evaluates to true when the amount bought of a {@Coin} is either greater than or less than + * (depending on the amount comparator) the amount specified. + */ +public class DollarsBoughtCondition extends AmountCondition { + + public static final TokenType PREFIX = PREFIX_BOUGHT; + public static final TokenType PARAMETER_TYPE = NUM; + + public DollarsBoughtCondition(Amount amount, BiPredicate amountComparator) { + super(amount, amountComparator); + } + + @Override + public boolean test(Coin coin) { + return amountComparator.test(coin.getTotalDollarsBought(), amount); + } +} +``` +###### \java\seedu\address\logic\conditions\DollarsSoldCondition.java +``` java +/** + * Represents a predicate that evaluates to true when the amount bought of a {@Coin} is either greater than or less than + * (depending on the amount comparator) the amount specified. + */ +public class DollarsSoldCondition extends AmountCondition { + + public static final TokenType PREFIX = PREFIX_SOLD; + public static final TokenType PARAMETER_TYPE = NUM; + + public DollarsSoldCondition(Amount amount, BiPredicate amountComparator) { + super(amount, amountComparator); + } + + @Override + public boolean test(Coin coin) { + return amountComparator.test(coin.getTotalDollarsSold(), amount); + } +} +``` +###### \java\seedu\address\logic\conditions\MadeCondition.java +``` java +/** + * Represents a predicate that evaluates to true when the amount made (dollar profit) of a {@Coin} is either + * greater than or less than (depending on the amount comparator) the amount specified. + */ +public class MadeCondition extends AmountCondition { + + public static final TokenType PREFIX = PREFIX_MADE; + public static final TokenType PARAMETER_TYPE = NUM; + + public MadeCondition(Amount amount, BiPredicate amountComparator) { + super(amount, amountComparator); + } + + @Override + public boolean test(Coin coin) { + return amountComparator.test(coin.getTotalProfit(), amount); + } +} +``` +###### \java\seedu\address\logic\conditions\TagCondition.java +``` java +/** + * Represents a predicate that evaluates to true when a {@Coin} contains the {@tag} specified. + */ +public class TagCondition implements Predicate { + + public static final TokenType PREFIX = PREFIX_TAG; + public static final TokenType PARAMETER_TYPE = STRING; + + private Tag tag; + + public TagCondition(Tag tag) { + this.tag = tag; + } + + @Override + public boolean test(Coin coin) { + return coin.getTags().contains(tag); + } +} +``` +###### \java\seedu\address\logic\conditions\WorthCondition.java +``` java +/** + * Represents a predicate that evaluates to true when the worth of a {@Coin} is either greater than or less than + * (depending on the amount comparator) the amount specified. + */ +public class WorthCondition extends AmountCondition { + + public static final TokenType PREFIX = PREFIX_WORTH; + + public WorthCondition(Amount amount, BiPredicate amountComparator) { + super(amount, amountComparator); + } + + @Override + public boolean test(Coin coin) { + return amountComparator.test(coin.getDollarsWorth(), amount); + } +} +``` +###### \java\seedu\address\logic\parser\ArgumentTokenizer.java +``` java + private static final int STRING_BUILDER_OFFSET = 1; + private static final String CAPTURING_GROUP_REGEX_PATTERN = "|(?<%s>%s)"; + private static final TokenType[] DEFAULT_TOKEN_TYPES = { + TokenType.BINARYBOOL, TokenType.UNARYBOOL, TokenType.LEFTPARENTHESES, TokenType.RIGHTPARENTHESES, + TokenType.COMPARATOR, TokenType.DECIMAL, TokenType.NUM, TokenType.STRING, TokenType.SLASH, + TokenType.WHITESPACE, TokenType.NEWLINE, TokenType.ELSE + }; + + private static String getCapturingGroupRegexPatternFromTokenType(TokenType type) { + return String.format(CAPTURING_GROUP_REGEX_PATTERN, type.typeName, type.regex); + } + + private static String getDefaultRegexPatternString() { + StringBuilder regexPatternBuffer = new StringBuilder(); + for (TokenType defaultTokenType : DEFAULT_TOKEN_TYPES) { + regexPatternBuffer.append(getCapturingGroupRegexPatternFromTokenType(defaultTokenType)); + } + return regexPatternBuffer.toString(); + } + + private static String getPatternString(TokenType... tokenTypes) { + StringBuilder regexPatternBuffer = new StringBuilder(); + for (TokenType type : tokenTypes) { + regexPatternBuffer.append(getCapturingGroupRegexPatternFromTokenType(type)); + } + regexPatternBuffer.append(getDefaultRegexPatternString()); + return regexPatternBuffer.substring(STRING_BUILDER_OFFSET); + } + + private static Pattern buildPatternFromTokenTypes(TokenType... tokenTypes) { + return Pattern.compile(getPatternString(tokenTypes)); + } + + private static List getTokenTypeList(TokenType... tokenTypes) { + ArrayList tokenTypeList = new ArrayList(); + tokenTypeList.addAll(Arrays.asList(tokenTypes)); + tokenTypeList.addAll(Arrays.asList(DEFAULT_TOKEN_TYPES)); + return tokenTypeList; + } + + /** + * Lexically analyses and tokenizes a string of arguments based on the {@code TokenType} specification. + * @return a list of {@code Token} based on the argument string provided in reverse order. + */ + private static List lex(String args, TokenType... prefixTokenTypes) { + + List typeList = getTokenTypeList(prefixTokenTypes); + List tokenList = new ArrayList(); + Pattern pattern = buildPatternFromTokenTypes(prefixTokenTypes); + Matcher m = pattern.matcher(args); + while (m.find()) { + for (TokenType type : typeList) { + if (m.group(type.typeName) == null) { + continue; + } + tokenList.add(new Token(type, m.group(type.typeName))); + } + } + // Add in an EOF type Token as a delimiter. + tokenList.add(new Token(TokenType.EOF, "")); + return tokenList; + } +``` +###### \java\seedu\address\logic\parser\ArgumentTokenizer.java +``` java + private static String extractPreambleString(List tokenList, PrefixTokenPosition currentPrefixToken) { + return extractArgumentsToString(tokenList, + new PrefixTokenPosition(TokenType.STRING, -1), + currentPrefixToken); + } + + /** + * Returns the trimmed value of the argument in the arguments string specified by {@code currentPrefixToken}. + * The end position of the value is determined by {@code nextPrefixToken}. + */ + private static String extractArgumentsToString(List tokenList, PrefixTokenPosition currentPrefixToken, + PrefixTokenPosition nextPrefixToken) { + List subTokenList = tokenList.subList(currentPrefixToken.getStartPosition() + 1, + nextPrefixToken.getStartPosition()); + return listOfTokensToString(subTokenList); + } + + /** + * Returns a String representation of a list of tokens with one space in between each token's string value. + */ + private static String listOfTokensToString(List tokenList) { + + StringBuilder stringBuilder = new StringBuilder(); + for (Token token : tokenList) { + stringBuilder.append(String.format("%s", token.getPattern())); + } + return stringBuilder.toString().trim(); + } +``` +###### \java\seedu\address\logic\parser\ConditionGenerator.java +``` java +public class ConditionGenerator { + + private TokenStack tokenStack; + + public ConditionGenerator(TokenStack tokenStack) { + this.tokenStack = tokenStack; + this.tokenStack.resetStack(); + } + + /** + * @return Generates a predicate on {@code Coin} objects based on the argument represented by the token stack. + * @throws IllegalValueException + */ + public Predicate generate() throws IllegalValueException { + return expression(); + } + + /** + * @return Generates a predicate on {@code Coin} objects based on the current EXPRESSION. (see DeveloperGuide.adoc) + * @throws IllegalValueException + */ + Predicate expression() throws IllegalValueException { + Predicate condition = term(); + while (tokenStack.matchTokenType(TokenType.BINARYBOOL)) { + Token operatorToken = tokenStack.popToken(); + Predicate secondCondition = term(); + switch (operatorToken.getPattern()) { + case " AND ": + condition = condition.and(secondCondition); + break; + case " OR ": + condition = condition.or(secondCondition); + break; + default: + break; + } + } + return condition; + } + + /** + * @return Generates a predicate on {@code Coin} objects based on the current TERM. (see DeveloperGuide.adoc) + * @throws IllegalValueException + */ + Predicate term() throws IllegalValueException { + Predicate condition; + if (tokenStack.matchAndPopTokenType(TokenType.LEFTPARENTHESES)) { + condition = expression(); + tokenStack.matchAndPopTokenType(TokenType.RIGHTPARENTHESES); + return condition; + } else if (tokenStack.matchAndPopTokenType(TokenType.UNARYBOOL)) { + return term().negate(); + } + return cond(); + } + + /** + * @return Generates a predicate on {@code Coin} objects based on the current COND. (see DeveloperGuide.adoc) + * @throws IllegalValueException + */ + Predicate cond() throws IllegalValueException { + return getPredicateFromPrefix(tokenStack.popToken().getType()); + } + + /** + * @param type + * @return a base predicate based on the prefix that is currently at the top of the stack. + * @throws IllegalValueException + */ + Predicate getPredicateFromPrefix(TokenType type) throws IllegalValueException { + BiPredicate amountComparator; + Amount specifiedAmount; + CompareMode compareMode = getCompareModeFromType(type); + switch (type) { + case PREFIX_HELD: + amountComparator = getAmountComparatorFromToken(tokenStack.popToken()); + specifiedAmount = ParserUtil.parseAmount(tokenStack.popToken().getPattern()); + return new AmountHeldCondition(specifiedAmount, amountComparator); + + case PREFIX_SOLD: + amountComparator = getAmountComparatorFromToken(tokenStack.popToken()); + specifiedAmount = ParserUtil.parseAmount(tokenStack.popToken().getPattern()); + return new DollarsSoldCondition(specifiedAmount, amountComparator); + + case PREFIX_BOUGHT: + amountComparator = getAmountComparatorFromToken(tokenStack.popToken()); + specifiedAmount = ParserUtil.parseAmount(tokenStack.popToken().getPattern()); + return new DollarsBoughtCondition(specifiedAmount, amountComparator); + + case PREFIX_MADE: + amountComparator = getAmountComparatorFromToken(tokenStack.popToken()); + specifiedAmount = ParserUtil.parseAmount(tokenStack.popToken().getPattern()); + return new MadeCondition(specifiedAmount, amountComparator); + + case PREFIX_PRICE: + case PREFIX_PRICE_RISE: + case PREFIX_PRICE_FALL: + amountComparator = getAmountComparatorFromToken(tokenStack.popToken()); + specifiedAmount = ParserUtil.parseAmount(tokenStack.popToken().getPattern()); + if (compareMode == null) { + return new CurrentPriceCondition(specifiedAmount, amountComparator); + } else { + return new CurrentPriceChangeCondition(specifiedAmount, amountComparator, compareMode); + } + + case PREFIX_WORTH: + case PREFIX_WORTH_RISE: + case PREFIX_WORTH_FALL: + amountComparator = getAmountComparatorFromToken(tokenStack.popToken()); + specifiedAmount = ParserUtil.parseAmount(tokenStack.popToken().getPattern()); + if (compareMode == null) { + return new WorthCondition(specifiedAmount, amountComparator); + } else { + return new WorthChangeCondition(specifiedAmount, amountComparator, compareMode); + } + + case PREFIX_CODE: + return new CodeCondition(tokenStack.popToken().getPattern()); + + case PREFIX_TAG: + Tag tag = ParserUtil.parseTag(tokenStack.popToken().getPattern()); + return new TagCondition(tag); + + default: + assert false; + return null; + } + } + + private CompareMode getCompareModeFromType(TokenType type) { + switch (type) { + case PREFIX_PRICE_RISE: + case PREFIX_WORTH_RISE: + return CompareMode.RISE; + + case PREFIX_PRICE_FALL: + case PREFIX_WORTH_FALL: + return CompareMode.FALL; + + default: + return null; + } + } + + private static BiPredicate getAmountComparatorFromToken(Token token) { + switch (token.getPattern()) { + case "=": + return (amount1, amount2) -> (amount1.compareTo(amount2) == 0); + case ">": + return (amount1, amount2) -> (amount1.compareTo(amount2) > 0); + case "<": + return (amount1, amount2) -> (amount1.compareTo(amount2) < 0); + default: + return null; + } + } +} +``` +###### \java\seedu\address\logic\parser\ConditionSemanticParser.java +``` java +/** + * Parses tokenized boolean logic statements to verify correctness + */ +public class ConditionSemanticParser { + + private TokenStack tokenStack; + + public ConditionSemanticParser(TokenStack tokenStack) { + this.tokenStack = tokenStack; + this.tokenStack.resetStack(); + } + + public TokenType getExpectedType() { + return this.tokenStack.getLastExpectedType(); + } + + public TokenType getActualType() { + return this.tokenStack.getActualType(); + } + + /** + * Parses the input as a token stack semantically. + * @return true if the input is semantically valid. + */ + public boolean parse() { + while (!tokenStack.isEmpty()) { + TokenType peekType = tokenStack.popToken().getType(); + if (TokenType.isPrefixType(peekType) && !hasCorrectParameterType(peekType)) { + return false; + } + } + return true; + } + + /** + * Checks to see each the prefix type is followed by the appropriate parameter types. + * @param type + * @return true if the prefix type is followed by the appropriate parameter types. + */ + private boolean hasCorrectParameterType(TokenType type) { + switch (type) { + case PREFIX_HELD: + case PREFIX_SOLD: + case PREFIX_BOUGHT: + case PREFIX_MADE: + case PREFIX_PRICE: + case PREFIX_PRICE_RISE: + case PREFIX_PRICE_FALL: + case PREFIX_WORTH: + case PREFIX_WORTH_RISE: + case PREFIX_WORTH_FALL: + return hasNumericalParameter(); + case PREFIX_CODE: + case PREFIX_TAG: + return hasStringParameter(); + default: + return false; + } + } + + /** + * Checks if the next two tokens are a comparator followed by a number. + * @return true if the next two tokens are a comparator followed by a number. + */ + private boolean hasNumericalParameter() { + return tokenStack.matchAndPopTokenType(TokenType.COMPARATOR) + && (tokenStack.matchAndPopTokenType(TokenType.NUM) + || tokenStack.matchAndPopTokenType(TokenType.DECIMAL)); + + } + + /** + * Checks if the next next token is a string. + * @return true if the next token is a string. + */ + private boolean hasStringParameter() { + return tokenStack.matchAndPopTokenType(TokenType.STRING); + } + + +} +``` +###### \java\seedu\address\logic\parser\ConditionSyntaxParser.java +``` java +public class ConditionSyntaxParser { + + private TokenStack tokenStack; + + public ConditionSyntaxParser(TokenStack tokenStack) { + this.tokenStack = tokenStack; + this.tokenStack.resetStack(); + } + + public TokenType getExpectedType() { + return this.tokenStack.getLastExpectedType(); + } + + public TokenType getActualType() { + return this.tokenStack.getActualType(); + } + + /** + * Parses the token stack against the boolean logic grammar loaded into the tokenStack + * @return correctness of the tokenStack + */ + public boolean parse() { + return expression() + && tokenStack.matchAndPopTokenType(TokenType.EOF); + } + + /** + * Matches the tokenStack against the expression grammar rule + * @return true if the tokenStack was correct + */ + boolean expression() { + if (!term()) { + return false; + } + while (tokenStack.matchAndPopTokenType(TokenType.BINARYBOOL)) { + if (!term()) { + return false; + } + } + return true; + } + + /** + * Matches the tokenStack against the term grammar rule + * @return true if the tokenStack was correct + */ + boolean term() { + + if (tokenStack.matchAndPopTokenType(TokenType.LEFTPARENTHESES)) { + if (!expression()) { + return false; + } + return tokenStack.matchAndPopTokenType(TokenType.RIGHTPARENTHESES); + } else if (tokenStack.matchAndPopTokenType(TokenType.UNARYBOOL)) { + return term(); + } + return cond(); + } + + /** + * Matches the tokenStack against the cond grammar rule + * @return true if the tokenStack was correct + */ + boolean cond() { + if (!isPrefix()) { + return false; + } + + tokenStack.matchAndPopTokenType(TokenType.COMPARATOR); + + return tokenStack.matchAndPopTokenType(TokenType.NUM) + || tokenStack.matchAndPopTokenType(TokenType.STRING) + || tokenStack.matchAndPopTokenType(TokenType.DECIMAL); + } + + /** + * Checks if the top of the tokenStack is currently a prefix TokenType. + */ + private boolean isPrefix() { + if (!tokenStack.isEmpty() && TokenType.isPrefixType(tokenStack.getActualType())) { + tokenStack.popToken(); + return true; + } + // If it was not a PREFIX, we do this to set the last expected type to some PREFIX type. + tokenStack.matchTokenType(TokenType.PREFIX_AMOUNT); + return false; + } + +} +``` +###### \java\seedu\address\logic\parser\ParserUtil.java +``` java + /** + * Parses a {@code String condition} represented by a {@code TokenStack} into a {@code Predicate}. + * @param argumentTokenStack a {@code TokenStack} representing the tokenized argument. + * @return a predicate representing the argument + * @throws IllegalValueException if the given tag names or numbers as parameters are invalid + * and if the argument is either syntactically or semantically invalid. + */ + public static Predicate parseCondition(TokenStack argumentTokenStack) + throws IllegalValueException { + requireNonNull(argumentTokenStack); + TokenType expectedTokenType; + TokenType actualTokenType; + + ConditionSyntaxParser conditionSyntaxParser = new ConditionSyntaxParser(argumentTokenStack); + if (!conditionSyntaxParser.parse()) { + expectedTokenType = conditionSyntaxParser.getExpectedType(); + actualTokenType = conditionSyntaxParser.getActualType(); + logger.warning(String.format(MESSAGE_CONDITION_ARGUMENT_INVALID_SYNTAX, "Syntactic", + expectedTokenType.description, actualTokenType.typeName)); + throw new ParseException("command arguments invalid."); + } + + ConditionSemanticParser conditionSemanticParser = new ConditionSemanticParser(argumentTokenStack); + if (!conditionSemanticParser.parse()) { + expectedTokenType = conditionSemanticParser.getExpectedType(); + actualTokenType = conditionSemanticParser.getActualType(); + logger.warning(String.format(MESSAGE_CONDITION_ARGUMENT_INVALID_SYNTAX, "Semantic", + expectedTokenType.description, actualTokenType.typeName)); + throw new ParseException("command arguments invalid."); + } + + ConditionGenerator conditionGenerator = new ConditionGenerator(argumentTokenStack); + return conditionGenerator.generate(); + } + //author@@ +} +``` +###### \java\seedu\address\logic\parser\Token.java +``` java +package seedu.address.logic.parser; + +/** + * Represents the token type that that portions of the string can be grouped into. + */ +public class Token { + private TokenType type; + private String pattern; + + public Token(TokenType type, String pattern) { + this.type = type; + this.pattern = pattern; + } + + public TokenType getType() { + return this.type; + } + + public boolean hasType(TokenType type) { + return this.type == type; + } + + public String getPattern() { + return this.pattern; + } + + @Override + public String toString() { + return String.format("(%s,%s)", pattern, this.type.name()); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof Token)) { + return false; + } + + Token otherToken = (Token) other; + return otherToken.getType().equals(this.type) + && otherToken.getPattern().equals(this.pattern); + + } +} +``` +###### \java\seedu\address\logic\parser\TokenStack.java +``` java +package seedu.address.logic.parser; + +import static seedu.address.logic.parser.TokenType.WHITESPACE; + +import java.util.EmptyStackException; +import java.util.List; + + +/** + * Represents stack of Token objects. + */ +public class TokenStack { + private List tokenList; + private TokenType lastExpectedType; + private int tokenHeadIndex; + + public TokenStack(List tokenList) { + tokenList.removeIf(t -> t.hasType(WHITESPACE)); + this.tokenList = tokenList; + tokenHeadIndex = 0; + lastExpectedType = null; + } + + /** + * Matches the type with the top token on the tokenList and pops it if they are they same. + * @param type the TokenType to compare the top token with. + * @return true if the types are the same, false if either the stack is expended or they are not the same type + */ + public boolean matchAndPopTokenType(TokenType type) throws EmptyStackException { + lastExpectedType = type; + if (tokenHeadIndex >= tokenList.size()) { + throw new EmptyStackException(); + } + if (tokenList.get(tokenHeadIndex).getType() == type) { + tokenHeadIndex++; + return true; + } + return false; + } + + /** + * Matches the type with the top token on the tokenList. + * @param type the TokenType to compare the top token with. + * @return true if the types are the same. + * @throws EmptyStackException if the stack is empty + */ + public boolean matchTokenType(TokenType type) throws EmptyStackException { + lastExpectedType = type; + if (tokenHeadIndex >= tokenList.size()) { + throw new EmptyStackException(); + } + return tokenList.get(tokenHeadIndex).getType() == type; + } + + /** + * @return the {@code Token} at the top of the stack + * @throws EmptyStackException if the stack is empty + */ + public Token popToken() throws EmptyStackException { + if (tokenHeadIndex >= tokenList.size()) { + throw new EmptyStackException(); + } + return tokenList.get(tokenHeadIndex++); + } + + /** + * Returns the Token at the top of the stack without removing it + * @return the Token at the top of the stack + * @throws EmptyStackException if the stack is empty + */ + public Token peekToken() throws EmptyStackException { + if (tokenHeadIndex >= tokenList.size()) { + throw new EmptyStackException(); + } + return tokenList.get(tokenHeadIndex); + } + + /** + * Resets the {@code TokenStack} to restore all the popped Tokens + */ + public void resetStack() { + tokenHeadIndex = 0; + } + + public boolean isEmpty() { + return tokenHeadIndex == tokenList.size(); + } + + public List getTokenList() { + return tokenList; + } + + /** + * @return The last TokenType that was checked. + */ + public TokenType getLastExpectedType() { + return lastExpectedType; + } + + /** + * @return The TokenType of token currently on top of the stack. + * @throws EmptyStackException if the stack is empty. + */ + public TokenType getActualType() throws EmptyStackException { + if (tokenHeadIndex >= tokenList.size()) { + throw new EmptyStackException(); + } + return tokenList.get(tokenHeadIndex).getType(); + } +} +``` +###### \java\seedu\address\logic\parser\TokenType.java +``` java +package seedu.address.logic.parser; + +/** + * Represents the possible types a token can take, along with the regular expression it is specified by. + */ +public enum TokenType { + /* Boolean Logic Operators */ + BINARYBOOL(" OR | AND ", "BINARYBOOL", "a boolean operator"), + UNARYBOOL("NOT ", "UNARYBOOL", "a NOT operator"), + LEFTPARENTHESES("\\(", "LEFTPARENTHESES", "a left parentheses"), + RIGHTPARENTHESES("\\)", "RIGHTPARENTHESES", "a left parentheses"), + COMPARATOR(">|=|<", "COMPARATOR", "a comparator, e.g. >"), + + /* String values */ + PREFIX_CODE("c/", "CPREFIX", "a prefix"), + PREFIX_NAME("n/", "NPREFIX", "a prefix"), + PREFIX_TAG("t/", "TPREFIX", "a prefix"), + + /* Numerical values */ + PREFIX_AMOUNT("a/", "APREFIX", "a prefix"), + // Below used for find/notify conditions + PREFIX_BOUGHT("b/", "BPREFIX", "a prefix"), + PREFIX_HELD("h/", "HPREFIX", "a prefix"), + PREFIX_MADE("m/", "MPREFIX", "a prefix"), + PREFIX_PRICE_RISE("p/\\+", "PRPREFIX", "a prefix"), + PREFIX_PRICE_FALL("p/\\-", "PFPREFIX", "a prefix"), + PREFIX_PRICE("p/", "PPREFIX", "a prefix"), + PREFIX_SOLD("s/", "SPREFIX", "a prefix"), + PREFIX_WORTH_RISE("w/\\+", "WRPREFIX", "a prefix"), + PREFIX_WORTH_FALL("w/\\-", "WFPREFIX", "a prefix"), + PREFIX_WORTH("w/", "WPREFIX", "a prefix"), + + /* Value components */ + DECIMAL("\\-?[0-9]+\\.[0-9]+", "DECIMAL", "a decimal number"), + NUM("\\-?[0-9]+", "NUM", "an integer"), + STRING("[A-Za-z\\^\\-\\@\\./]+", "STRING", "a string"), + SLASH("/", "SLASH", "a slash"), + WHITESPACE("\\s", "WHITESPACE", "some white space"), + NEWLINE("\\n", "NEWLINE", "a newline"), + ELSE(".+", "ELSE", "some unknown character"), + EOF("[^\\w\\W]", "EOF", "some end of the argument"); + + final String typeName; + final String regex; + final String description; + + TokenType(final String regex, final String typeName, final String description) { + this.regex = regex; + this.typeName = typeName; + this.description = description; + } + + public String toString() { + return this.regex; + } + + /** + * Checks if the {@code type} is a prefix type + * @param type the type to be checked + */ + public static boolean isPrefixType(TokenType type) { + return type == PREFIX_AMOUNT + || type == PREFIX_BOUGHT + || type == PREFIX_CODE + || type == PREFIX_HELD + || type == PREFIX_MADE + || type == PREFIX_NAME + || type == PREFIX_PRICE_RISE + || type == PREFIX_PRICE_FALL + || type == PREFIX_PRICE + || type == PREFIX_SOLD + || type == PREFIX_TAG + || type == PREFIX_WORTH_RISE + || type == PREFIX_WORTH_FALL + || type == PREFIX_WORTH; + } +} +``` diff --git a/collated/functional/ewaldhew.md b/collated/functional/ewaldhew.md new file mode 100644 index 000000000000..f2f17049485b --- /dev/null +++ b/collated/functional/ewaldhew.md @@ -0,0 +1,1732 @@ +# ewaldhew +###### \java\seedu\address\commons\events\model\CoinChangedEvent.java +``` java +package seedu.address.commons.events.model; + +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.events.BaseEvent; +import seedu.address.model.coin.Coin; + +/** + * Indicates some coin data in the model has changed. + */ +public class CoinChangedEvent extends BaseEvent { + + private static final String FORMAT_STRING = "Coin changed [%1$s] -> [%2$s]"; + + public final Index index; + public final Coin data; + + public CoinChangedEvent(Index index, Coin oldCoin, Coin newCoin) { + requireAllNonNull(index, oldCoin, newCoin); + assert(newCoin.getPrevState().equals(oldCoin)); + this.index = index; + this.data = newCoin; + } + + @Override + public String toString() { + return String.format(FORMAT_STRING, data.getPrevState(), data); + } +} +``` +###### \java\seedu\address\commons\events\ui\ShowNotificationRequestEvent.java +``` java +package seedu.address.commons.events.ui; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.events.BaseEvent; + +/** + * An event requesting to spawn a pop-up notification with the given message. + */ +public class ShowNotificationRequestEvent extends BaseEvent { + + private static final String MESSAGE_NOTIFYING = "Notifying about: %1$s triggers %2$s"; + + /** The index of the coin that triggered this notification */ + public final Index targetIndex; + + /** The code of the coin that triggered this notification */ + public final String codeString; + + private final String message; + + public ShowNotificationRequestEvent(String message, Index index, String codeString) { + this.message = message; + this.targetIndex = index; + this.codeString = codeString; + } + + @Override + public String toString() { + return String.format(MESSAGE_NOTIFYING, codeString, message); + } +} +``` +###### \java\seedu\address\commons\events\ui\ShowNotifManRequestEvent.java +``` java +package seedu.address.commons.events.ui; + +import javafx.collections.ObservableList; +import seedu.address.commons.events.BaseEvent; +import seedu.address.model.rule.Rule; + +/** + * An event requesting to view the notification manager. + */ +public class ShowNotifManRequestEvent extends BaseEvent { + + public final ObservableList data; + + public ShowNotifManRequestEvent(ObservableList data) { + this.data = data; + } + + @Override + public String toString() { + return "Show notification manager: " + data.toString(); + } + +} +``` +###### \java\seedu\address\logic\commands\BuyCommand.java +``` java +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.TokenType.PREFIX_AMOUNT; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_COINS; + +import java.util.List; +import java.util.Objects; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.coin.Amount; +import seedu.address.model.coin.Coin; +import seedu.address.model.coin.exceptions.CoinNotFoundException; +import seedu.address.model.coin.exceptions.DuplicateCoinException; + +/** + * Adds value to an existing coin in the book. + */ +public class BuyCommand extends UndoableCommand { + + public static final String COMMAND_WORD = "buy"; + public static final String COMMAND_ALIAS = "b"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Add value to the coin account identified " + + "by the index number used in the last coin listing or its code. " + + "Parameters: TARGET " + + PREFIX_AMOUNT + "AMOUNT\n" + + "Example: " + COMMAND_WORD + " 1 " + PREFIX_AMOUNT + "50.0"; + + public static final String MESSAGE_BUY_COIN_SUCCESS = "Bought: %1$s"; + public static final String MESSAGE_NOT_BOUGHT = "Invalid code or amount entered."; + + private final CommandTarget target; + private final Amount amountToAdd; + + private Coin coinToEdit; + private Coin editedCoin; + + /** + * @param target in the filtered coin list to change + * @param amountToAdd to the coin + */ + public BuyCommand(CommandTarget target, Amount amountToAdd) { + requireNonNull(target); + + this.target = target; + this.amountToAdd = amountToAdd; + } + + @Override + public CommandResult executeUndoableCommand() throws CommandException { + try { + model.updateCoin(coinToEdit, editedCoin); + } catch (DuplicateCoinException dpe) { + throw new CommandException("Unexpected code path!"); + } catch (CoinNotFoundException pnfe) { + throw new AssertionError("The target coin cannot be missing"); + } + model.updateFilteredCoinList(PREDICATE_SHOW_ALL_COINS); + return new CommandResult(String.format(MESSAGE_BUY_COIN_SUCCESS, editedCoin)); + } + + @Override + protected void preprocessUndoableCommand() throws CommandException { + List lastShownList = model.getFilteredCoinList(); + + try { + Index index = target.toIndex(model.getFilteredCoinList()); + coinToEdit = lastShownList.get(index.getZeroBased()); + editedCoin = createEditedCoin(coinToEdit, amountToAdd); + } catch (IndexOutOfBoundsException oobe) { + throw new CommandException(Messages.MESSAGE_INVALID_COMMAND_TARGET); + } + } + + /** + * Creates and returns a {@code Coin} with the details of {@code coinToEdit} + */ + private static Coin createEditedCoin(Coin coinToEdit, Amount amountToAdd) { + assert coinToEdit != null; + + Coin editedCoin = new Coin(coinToEdit); + editedCoin.addTotalAmountBought(amountToAdd); + + return editedCoin; + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof BuyCommand)) { + return false; + } + + // state check + BuyCommand e = (BuyCommand) other; + return target.equals(e.target) + && amountToAdd.equals(e.amountToAdd) + && Objects.equals(coinToEdit, e.coinToEdit); + } +} +``` +###### \java\seedu\address\logic\commands\CommandTarget.java +``` java +package seedu.address.logic.commands; + +import javafx.collections.ObservableList; +import seedu.address.commons.core.index.Index; +import seedu.address.model.coin.Code; +import seedu.address.model.coin.Coin; + +/** + * Represents a command target specified in one of the available modes (union type). + */ +public class CommandTarget { + + /** + * All possible target representation modes + */ + private enum Mode { + INDEX, + CODE + } + + private final Mode mode; + + private Index index; + private Code code; + + public CommandTarget(Index index) { + mode = Mode.INDEX; + this.index = index; + } + + public CommandTarget(Code code) { + mode = Mode.CODE; + this.code = code; + } + + /** + * @param coinList to obtain index information from + * @return + */ + public Index toIndex(ObservableList coinList) throws IndexOutOfBoundsException { + switch (mode) { + + case CODE: + // Also throws IndexOutOfBoundsException if code isn't found. + return Index.fromZeroBased(coinList.filtered(coin -> coin.getCode().equals(code)).getSourceIndex(0)); + case INDEX: + if (index.getZeroBased() >= coinList.size()) { + throw new IndexOutOfBoundsException(); + } + return index; + + default: + throw new RuntimeException("Unexpected code path!"); + } + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof CommandTarget)) { + return false; + } + + // state check + CommandTarget e = (CommandTarget) other; + return mode.equals(e.mode) + && ((mode == Mode.INDEX && index.equals(e.index)) + || (mode == Mode.CODE && code.equals(e.code))); + } +} +``` +###### \java\seedu\address\logic\commands\NotifyCommand.java +``` java +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.rule.NotificationRule; +import seedu.address.model.rule.exceptions.DuplicateRuleException; + +/** + * Adds a new notification with the specified conditions. + */ +public class NotifyCommand extends Command { + + public static final String COMMAND_WORD = "notify"; + public static final String COMMAND_ALIAS = "n"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a new notification to be triggered " + + "upon the specified rule. Rules are provided in the following format:\n" + + "Parameters: TARGET OPTION/VALUE [...] \n" + + "Example: " + COMMAND_WORD + " c/BTC AND p/>15000"; + + public static final String MESSAGE_SUCCESS = "Added: %1$s"; + public static final String MESSAGE_DUPLICATE_RULE = "This notification rule already exists!"; + + private final NotificationRule rule; + + public NotifyCommand(NotificationRule rule) { + requireNonNull(rule); + this.rule = rule; + } + + @Override + public CommandResult execute() throws CommandException { + requireNonNull(model); + try { + model.addRule(rule); + return new CommandResult(String.format(MESSAGE_SUCCESS, rule)); + } catch (DuplicateRuleException e) { + throw new CommandException(MESSAGE_DUPLICATE_RULE); + } + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof NotifyCommand) // instanceof handles nulls + && this.rule.equals(((NotifyCommand) other).rule); // state check + + } +} +``` +###### \java\seedu\address\logic\commands\SellCommand.java +``` java +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.TokenType.PREFIX_AMOUNT; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_COINS; + +import java.util.List; +import java.util.Objects; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.coin.Amount; +import seedu.address.model.coin.Coin; +import seedu.address.model.coin.exceptions.CoinNotFoundException; +import seedu.address.model.coin.exceptions.DuplicateCoinException; + +/** + * Removes value from an existing coin in the book. + */ +public class SellCommand extends UndoableCommand { + + public static final String COMMAND_WORD = "sell"; + public static final String COMMAND_ALIAS = "s"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Removes value from the coin account identified " + + "by the index number used in the last coin listing or its code. " + + "Parameters: TARGET " + + PREFIX_AMOUNT + "AMOUNT\n" + + "Example: " + COMMAND_WORD + " 1 " + PREFIX_AMOUNT + "50.0"; + + public static final String MESSAGE_SELL_COIN_SUCCESS = "Sold: %1$s"; + + private final CommandTarget target; + private final Amount amountToSell; + + private Coin coinToEdit; + private Coin editedCoin; + + /** + * @param target of the coin in the filtered coin list to change + * @param amountToSell of the coin + */ + public SellCommand(CommandTarget target, Amount amountToSell) { + requireNonNull(target); + + this.target = target; + this.amountToSell = amountToSell; + } + + @Override + public CommandResult executeUndoableCommand() throws CommandException { + try { + model.updateCoin(coinToEdit, editedCoin); + } catch (DuplicateCoinException dpe) { + throw new CommandException("Unexpected code path!"); + } catch (CoinNotFoundException pnfe) { + throw new AssertionError("The target coin cannot be missing"); + } + model.updateFilteredCoinList(PREDICATE_SHOW_ALL_COINS); + return new CommandResult(String.format(MESSAGE_SELL_COIN_SUCCESS, editedCoin)); + } + + @Override + protected void preprocessUndoableCommand() throws CommandException { + List lastShownList = model.getFilteredCoinList(); + + try { + Index index = target.toIndex(model.getFilteredCoinList()); + coinToEdit = lastShownList.get(index.getZeroBased()); + editedCoin = createEditedCoin(coinToEdit, amountToSell); + } catch (IndexOutOfBoundsException oobe) { + throw new CommandException(Messages.MESSAGE_INVALID_COMMAND_TARGET); + } + } + + /** + * Creates and returns a {@code Coin} with the details of {@code coinToEdit} + */ + private static Coin createEditedCoin(Coin coinToEdit, Amount amountToSell) { + assert coinToEdit != null; + + Coin editedCoin = new Coin(coinToEdit); + editedCoin.addTotalAmountSold(amountToSell); + + return editedCoin; + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof SellCommand)) { + return false; + } + + // state check + SellCommand e = (SellCommand) other; + return target.equals(e.target) + && amountToSell.equals(e.amountToSell) + && Objects.equals(coinToEdit, e.coinToEdit); + } +} +``` +###### \java\seedu\address\logic\commands\SpawnNotificationCommand.java +``` java +package seedu.address.logic.commands; + +import seedu.address.commons.core.EventsCenter; +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.core.index.Index; +import seedu.address.commons.events.BaseEvent; +import seedu.address.commons.events.model.CoinChangedEvent; +import seedu.address.commons.events.ui.ShowNotificationRequestEvent; +import seedu.address.model.coin.Coin; + +/** + * Spawns a pop-up notification in the corner of the screen. + */ +public class SpawnNotificationCommand extends ActionCommand { + + private final String message; + private Coin jumpTo; + private Index index; + + public SpawnNotificationCommand(String message) { + this.message = message; + } + + @Override + public void setExtraData(Coin data, BaseEvent event) { + assert(event instanceof CoinChangedEvent); + + jumpTo = data; + index = ((CoinChangedEvent) event).index; + } + + @Override + public CommandResult execute() { + try { + EventsCenter.getInstance() + .post(new ShowNotificationRequestEvent(message, index, jumpTo.getCode().toString())); + } catch (IndexOutOfBoundsException e) { + // Should not throw here, but do not crash anyway + LogsCenter.getLogger(this.getClass()).severe("Encountered invalid index in rule execute."); + } + return new CommandResult(""); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof SpawnNotificationCommand // instanceof handles nulls + && this.message.equals(((SpawnNotificationCommand) other).message)); // state check + } +} +``` +###### \java\seedu\address\logic\commands\SyncCommand.java +``` java + ArrayList historicalPriceRawData = getHistoricalPriceRawData(code); + + List historicalPrices = + historicalPriceRawData.stream() + .map(obj -> new Amount(obj.get("close").getAsString())) + .collect(Collectors.toList()); + List historicalTimes = + historicalPriceRawData.stream() + .map(obj -> obj.get("time").getAsString()) + .collect(Collectors.toList()); + newPrice.setHistorical(historicalPrices, historicalTimes); + + priceObjs.put(code, newPrice); + } + return priceObjs; + } + + /** + * Fetches the raw data for single code historical prices + */ + private ArrayList getHistoricalPriceRawData(String code) { + List histoPriceParams = buildParams(code, HISTORICAL); + JsonElement histoPriceArray = getJsonObject(historicalPriceApiUrl, histoPriceParams).get("Data"); + return new Gson().fromJson(histoPriceArray.toString(), new TypeToken>(){}.getType()); + } + +} +``` +###### \java\seedu\address\logic\conditions\AmountChangeCondition.java +``` java +package seedu.address.logic.conditions; + +import java.util.function.BiPredicate; + +import seedu.address.model.coin.Amount; + +/** + * Represents the predicates that evaluate two Amount objects. Is + */ +public abstract class AmountChangeCondition extends AmountCondition { + + /** + * Indicates whether to compare absolute or change + */ + public enum CompareMode { + RISE, + FALL + } + + public final CompareMode compareMode; + + public AmountChangeCondition(Amount amount, BiPredicate amountComparator, CompareMode compareMode) { + super(amount, amountComparator); + this.compareMode = compareMode; + } +} +``` +###### \java\seedu\address\logic\conditions\CurrentPriceChangeCondition.java +``` java +package seedu.address.logic.conditions; + +import static seedu.address.logic.parser.TokenType.NUM; +import static seedu.address.logic.parser.TokenType.PREFIX_PRICE; + +import java.util.function.BiPredicate; + +import seedu.address.commons.core.LogsCenter; +import seedu.address.logic.parser.TokenType; +import seedu.address.model.coin.Amount; +import seedu.address.model.coin.Coin; + +/** + * Represents a predicate that evaluates to true when the price of a {@Coin} is either greater than or less than + * (depending on the amount comparator) the amount specified. + */ +public class CurrentPriceChangeCondition extends AmountChangeCondition { + + public static final TokenType PREFIX = PREFIX_PRICE; + public static final TokenType PARAMETER_TYPE = NUM; + + + public CurrentPriceChangeCondition(Amount amount, + BiPredicate amountComparator, + CompareMode compareMode) { + super(amount, amountComparator, compareMode); + } + + @Override + public boolean test(Coin coin) { + switch (compareMode) { + case RISE: + return amountComparator.test(coin.getChangeFromPrev().getPrice().getCurrent(), amount); + case FALL: + return amountComparator.test(coin.getChangeToPrev().getPrice().getCurrent(), amount); + default: + LogsCenter.getLogger(this.getClass()).warning("Invalid compare mode!"); + return false; + } + } +} +``` +###### \java\seedu\address\logic\conditions\WorthChangeCondition.java +``` java +package seedu.address.logic.conditions; + +import static seedu.address.logic.parser.TokenType.PREFIX_WORTH; + +import java.util.function.BiPredicate; + +import seedu.address.commons.core.LogsCenter; +import seedu.address.logic.parser.TokenType; +import seedu.address.model.coin.Amount; +import seedu.address.model.coin.Coin; + +/** + * Represents a predicate that evaluates to true when the worth of a {@Coin} is either greater than or less than + * (depending on the amount comparator) the amount specified. + */ +public class WorthChangeCondition extends AmountChangeCondition { + + public static final TokenType PREFIX = PREFIX_WORTH; + + public WorthChangeCondition(Amount amount, BiPredicate amountComparator, CompareMode compareMode) { + super(amount, amountComparator, compareMode); + } + + @Override + public boolean test(Coin coin) { + switch (compareMode) { + case RISE: + return amountComparator.test(Amount.getDiff( + coin.getDollarsWorth(), coin.getPrevState().getDollarsWorth()), amount); + case FALL: + return amountComparator.test(Amount.getDiff( + coin.getPrevState().getDollarsWorth(), coin.getDollarsWorth()), amount); + default: + LogsCenter.getLogger(this.getClass()).warning("Invalid compare mode!"); + return false; + } + } +} +``` +###### \java\seedu\address\logic\parser\BuyCommandParser.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.TokenType.PREFIX_AMOUNT; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.logic.commands.BuyCommand; +import seedu.address.logic.commands.CommandTarget; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.coin.Amount; + +/** + * Parses input arguments and creates a new BuyCommand object + */ +public class BuyCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the BuyCommand + * and returns an BuyCommand object for execution. + * + * @throws ParseException if the user input does not conform the expected format + */ + public BuyCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenizeToArgumentMultimap(args, PREFIX_AMOUNT); + if (!argMultimap.arePrefixesPresent(PREFIX_AMOUNT) + || argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, BuyCommand.MESSAGE_USAGE)); + } + + try { + CommandTarget target = ParserUtil.parseTarget(argMultimap.getPreamble()); + Amount amountToAdd = ParserUtil.parseAmount(argMultimap.getValue(PREFIX_AMOUNT).get()); + return new BuyCommand(target, amountToAdd); + } catch (IllegalValueException ive) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, BuyCommand.MESSAGE_USAGE)); + } + + } + +} +``` +###### \java\seedu\address\logic\parser\NotifyCommandParser.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.TokenType.PREFIX_AMOUNT; +import static seedu.address.logic.parser.TokenType.PREFIX_BOUGHT; +import static seedu.address.logic.parser.TokenType.PREFIX_CODE; +import static seedu.address.logic.parser.TokenType.PREFIX_HELD; +import static seedu.address.logic.parser.TokenType.PREFIX_MADE; +import static seedu.address.logic.parser.TokenType.PREFIX_NAME; +import static seedu.address.logic.parser.TokenType.PREFIX_PRICE; +import static seedu.address.logic.parser.TokenType.PREFIX_PRICE_FALL; +import static seedu.address.logic.parser.TokenType.PREFIX_PRICE_RISE; +import static seedu.address.logic.parser.TokenType.PREFIX_SOLD; +import static seedu.address.logic.parser.TokenType.PREFIX_TAG; +import static seedu.address.logic.parser.TokenType.PREFIX_WORTH; +import static seedu.address.logic.parser.TokenType.PREFIX_WORTH_FALL; +import static seedu.address.logic.parser.TokenType.PREFIX_WORTH_RISE; + +import java.util.function.Predicate; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.logic.commands.NotifyCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.coin.Coin; +import seedu.address.model.rule.NotificationRule; + +/** + * Parses input arguments and creates a new NotifyCommand object + */ +public class NotifyCommandParser implements Parser { + + private static final TokenType[] EXPECTED_TOKEN_TYPES = { + PREFIX_AMOUNT, + PREFIX_BOUGHT, + PREFIX_CODE, + PREFIX_HELD, + PREFIX_MADE, + PREFIX_NAME, + PREFIX_PRICE_RISE, PREFIX_PRICE_FALL, PREFIX_PRICE, + PREFIX_SOLD, + PREFIX_TAG, + PREFIX_WORTH_RISE, PREFIX_WORTH_FALL, PREFIX_WORTH + }; + + /** + * Parses the given {@code String} of arguments in the context of the NotifyCommand + * and returns an NotifyCommand object for execution. + * + * @throws ParseException if the user input does not conform to the expected format + */ + public NotifyCommand parse(String args) throws ParseException { + try { + NotificationRule notifRule = new NotificationRule(args); + return new NotifyCommand(notifRule); + } catch (IllegalArgumentException e) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, NotifyCommand.MESSAGE_USAGE)); + } + } + + /** + * Parses a string representation of a notification condition + * @see ParserUtil#parseCondition(TokenStack) + */ + public static Predicate parseNotifyCondition(String args) + throws IllegalValueException { + requireNonNull(args); + return ParserUtil.parseCondition(ArgumentTokenizer.tokenizeToTokenStack(args, EXPECTED_TOKEN_TYPES)); + } + +} +``` +###### \java\seedu\address\logic\parser\SellCommandParser.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.TokenType.PREFIX_AMOUNT; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.logic.commands.CommandTarget; +import seedu.address.logic.commands.SellCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.coin.Amount; + +/** + * Parses input arguments and creates a new SellCommand object + */ +public class SellCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the BuyCommand + * and returns an BuyCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public SellCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenizeToArgumentMultimap(args, PREFIX_AMOUNT); + if (!argMultimap.arePrefixesPresent(PREFIX_AMOUNT) + || argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, SellCommand.MESSAGE_USAGE)); + } + + try { + CommandTarget target = ParserUtil.parseTarget(argMultimap.getPreamble()); + Amount amountToSell = ParserUtil.parseAmount(argMultimap.getValue(PREFIX_AMOUNT).get()); + return new SellCommand(target, amountToSell); + } catch (IllegalValueException ive) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, SellCommand.MESSAGE_USAGE)); + } + + } + +} +``` +###### \java\seedu\address\logic\RuleChecker.java +``` java +package seedu.address.logic; + +import java.util.logging.Logger; + +import com.google.common.eventbus.Subscribe; + +import seedu.address.commons.core.EventsCenter; +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.events.model.CoinChangedEvent; +import seedu.address.commons.events.model.RuleBookChangedEvent; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.ReadOnlyRuleBook; +import seedu.address.model.RuleBook; +import seedu.address.model.rule.NotificationRule; +import seedu.address.model.rule.Rule; + +/** + * Receives events to check against the rule book triggers. + */ +public class RuleChecker { + + private static final Logger logger = LogsCenter.getLogger(RuleChecker.class); + + private final RuleBook rules; + + public RuleChecker(ReadOnlyRuleBook rules) { + this.rules = new RuleBook(rules); + EventsCenter.getInstance().registerHandler(this); + } + + @Subscribe + public void handleRuleBookChangedEvent(RuleBookChangedEvent rbce) { + logger.info(LogsCenter.getEventHandlingLogMessage(rbce, "Reloading rule book...")); + this.rules.resetData(rbce.data); + } + + @Subscribe + public void handleCoinChangedEvent(CoinChangedEvent cce) { + for (Rule r : rules.getRuleList()) { + r.action.setExtraData(cce.data, cce); + + switch (r.type) { + case NOTIFICATION: + assert(r instanceof NotificationRule); + checkAndFire(r, cce.data); + break; + + default: + throw new RuntimeException("Unexpected code path!"); + } + } + } + + /** + * Checks the trigger condition against the provided object, then + * executes the command tied to it if it matches + * + * @param rule containing condition to check with + * @param data to check against + * @return Whether the command was successful. + */ + private static boolean checkAndFire(Rule rule, T data) { + if (!rule.condition.test(data)) { + return false; + } + + try { + rule.action.execute(); + logger.info(String.format(Rule.MESSAGE_FIRED, rule, data)); + return true; + } catch (CommandException e) { + logger.warning(e.getMessage()); + return false; + } + } +} +``` +###### \java\seedu\address\model\coin\Amount.java +``` java +package seedu.address.model.coin; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +import seedu.address.commons.core.LogsCenter; + +/** + * Represents the amount of the coin held in the address book. + */ +public class Amount implements Comparable { + + private static final String[] MAGNITUDE_CHAR = { "", "K", "M", "B", "T", "Q", "P", "S", "H" }; + private static final String MESSAGE_TOO_BIG = "This value can't be displayed as it is too big, " + + "total amount far exceeds circulating supply!\n" + + "Unfortunately, CoinBook cannot yet handle unorthodox usage [Coming in v2.0]"; + private static final String DISPLAY_TOO_BIG = "Err (see log)"; + + private BigDecimal value; + + /** + * Constructs an {@code Amount}. + */ + public Amount() { + this.value = new BigDecimal(0); + } + + /** + * Constructs an {@code Amount} with given value. + */ + public Amount(Amount amount) { + this.value = amount.value; + } + + /** + * Constructs an {@code Amount} with given value. + */ + public Amount(String value) { + this.value = new BigDecimal(value).setScale(8, RoundingMode.UP); + } + + /** + * Constructs an {@code Amount} with given value. + * For internal use only. + */ + private Amount(BigDecimal value) { + this.value = value; + } + + + /** + * Adds two amounts together and returns a new object. + * @return the sum of the two arguments + */ + public static Amount getSum(Amount first, Amount second) { + return new Amount(first.value.add(second.value)); + } + + /** + * Subtracts second from first and returns a new object. + * @return the difference of the two arguments + */ + public static Amount getDiff(Amount first, Amount second) { + return new Amount(first.value.subtract(second.value)); + } + + /** + * Multiplus two amounts together and returns a new object. + * @return the product of the two arguments + */ + public static Amount getMult(Amount first, Amount second) { + return new Amount(first.value.multiply(second.value)); + } + + /** + * Adds addAmount to the current value. + * + * @param addAmount amount to be added. + */ + public void addValue(Amount addAmount) { + value = value.add(addAmount.value); + } + + /** + * Gets the string representation of the full value. + * Use {@code toString} instead for display purposes. + * @see Amount#toString + */ + public String getValue() { + return value.toString(); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Amount // instanceof handles nulls + && this.value.compareTo(((Amount) other).value) == 0); // state check + } + + /** + * Gets the display string of the value. Displays up to 4 d.p. + * @return + */ + @Override + public String toString() { + // Calculate the magnitude, which is the nearest lower multiple of three, of digits + final int magnitude = value.compareTo(BigDecimal.ZERO) == 0 + ? 0 + : (value.precision() - value.scale()) / 3; + + if (0 < magnitude && magnitude < 2) { + return value.setScale(4, RoundingMode.UP).toPlainString(); + } else if (magnitude < MAGNITUDE_CHAR.length) { + // Shift the decimal point to keep the string printed at 7 digits max + return value.movePointLeft(magnitude * 3) + .setScale(4, RoundingMode.UP) + .toPlainString() + + MAGNITUDE_CHAR[magnitude]; + } else { + // We don't handle absurd cases specially for now + LogsCenter.getLogger(Amount.class).warning(MESSAGE_TOO_BIG); + return DISPLAY_TOO_BIG; + } + + } + + @Override + public int compareTo(Amount other) { + return value.compareTo(other.value); + } +} +``` +###### \java\seedu\address\model\coin\Coin.java +``` java + /** + * Gets the difference between two coins and makes a new coin record with that change. + * @return (final minus initial) as a coin, where the final coin is this + */ + public Coin getChangeFrom(Coin initialCoin) { + assert(initialCoin.code.equals(this.code)); + + return new Coin(initialCoin.code, + initialCoin.getTags(), + price.getChangeFrom(initialCoin.price), + Amount.getDiff(this.totalAmountBought, initialCoin.totalAmountBought), + Amount.getDiff(this.totalAmountSold, initialCoin.totalAmountSold), + Amount.getDiff(this.totalDollarsBought, initialCoin.totalDollarsBought), + Amount.getDiff(this.totalDollarsSold, initialCoin.totalDollarsSold)); + } + + public Coin getChangeFromPrev() { + return getChangeFrom(prevState); + } + + public Coin getChangeToPrev() { + return prevState.getChangeFrom(this); + } +``` +###### \java\seedu\address\model\coin\Price.java +``` java + public void setHistorical(List prices, List timestamps) { + historicalPrices = prices; + historicalTimeStamps = timestamps; + } + + public Price getChangeFrom(Price initial) { + Price result = new Price(); + result.currentPrice = Amount.getDiff(currentPrice, initial.currentPrice); + + return result; + } + + public List getHistoricalPrices() { + return historicalPrices; + } + + public List getHistoricalTimeStamps() { + return historicalTimeStamps; + } +``` +###### \java\seedu\address\model\ReadOnlyRuleBook.java +``` java +package seedu.address.model; + +import javafx.collections.ObservableList; +import seedu.address.model.rule.Rule; + +/** + * Unmodifiable view of a rule book + */ +public interface ReadOnlyRuleBook { + + /** + * Returns an unmodifiable view of the rules list. + * This list will not contain any duplicate rules. + */ + ObservableList getRuleList(); + +} +``` +###### \java\seedu\address\model\rule\exceptions\DuplicateRuleException.java +``` java +package seedu.address.model.rule.exceptions; + +import seedu.address.commons.exceptions.DuplicateDataException; + +/** + * Signals that the operation will result in duplicate Rule objects. + */ +public class DuplicateRuleException extends DuplicateDataException { + public DuplicateRuleException() { + super("Operation would result in duplicate rule"); + } +} +``` +###### \java\seedu\address\model\rule\NotificationRule.java +``` java +package seedu.address.model.rule; + +import seedu.address.logic.commands.SpawnNotificationCommand; +import seedu.address.logic.parser.NotifyCommandParser; +import seedu.address.model.coin.Coin; + +/** + * Represents a rule trigger for spawning notifications. + * The target object type is Coins. + */ +public class NotificationRule extends Rule { + + private static final ActionParser parseAction = SpawnNotificationCommand::new; + private static final ConditionParser parseCondition = NotifyCommandParser::parseNotifyCondition; + + public NotificationRule(String value) { + super(value, RuleType.NOTIFICATION, parseAction, parseCondition); + } + +} +``` +###### \java\seedu\address\model\rule\Rule.java +``` java +package seedu.address.model.rule; + +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.function.Predicate; +import java.util.logging.Logger; + +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.logic.commands.ActionCommand; + +/** + * Represents a Rule in the rule book. + * Guarantees: details are present and not null, field values are validated, immutable. + */ +public class Rule { + + public static final String MESSAGE_RULE_INVALID = "Rule description is invalid"; + public static final String MESSAGE_FIRED = "[Rule Match] %1$s <==> %2$s"; + private static final String RULE_FORMAT_STRING = "[%1$s]%2$s"; + + private static final Logger logger = LogsCenter.getLogger(Rule.class); + + public final RuleType type; + public final String description; + + public final ActionCommand action; + public final Predicate condition; + + protected Rule(String description, RuleType type, + ActionParser actionParser, + ConditionParser conditionParser) { + requireAllNonNull(description, type, actionParser, conditionParser); + this.description = description; + this.type = type; + this.action = actionParser.parse(description); + this.condition = validateAndCreateCondition(description, conditionParser); + } + + /** + * Uses the given parser to validate the condition string and create the condition object. + */ + private static Predicate validateAndCreateCondition(String conditionArgs, ConditionParser parser) { + try { + return parser.parse(conditionArgs); + } catch (IllegalValueException e) { + throw new IllegalArgumentException(MESSAGE_RULE_INVALID); + } + } + + @Override + public int hashCode() { + return super.hashCode(); + } + + @Override + public String toString() { + return String.format(RULE_FORMAT_STRING, type, description); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof Rule)) { + return false; + } + + Rule otherRule = (Rule) other; + return otherRule.description.equals(this.description) + && otherRule.action.equals(this.action); + } + + /** + * Represents a function type used to generate the action for this rule. + */ + @FunctionalInterface + protected interface ActionParser { + ActionCommand parse(String args); + } + + /** + * Represents a function type used to generate the trigger condition for this rule. + */ + @FunctionalInterface + protected interface ConditionParser { + Predicate parse(String args) throws IllegalValueException; + } + +} +``` +###### \java\seedu\address\model\rule\RuleType.java +``` java +package seedu.address.model.rule; + +/** + * Enumerates the possible types of rules in the RuleBook + */ +public enum RuleType { + NOTIFICATION +} +``` +###### \java\seedu\address\model\RuleBook.java +``` java +package seedu.address.model; + +import static java.util.Objects.requireNonNull; + +import java.util.List; +import java.util.Objects; + +import javafx.collections.ObservableList; +import seedu.address.model.rule.Rule; +import seedu.address.model.rule.UniqueRuleList; +import seedu.address.model.rule.exceptions.DuplicateRuleException; +import seedu.address.model.rule.exceptions.RuleNotFoundException; + +/** + * Stores a list of rules that can be used for notifications, etc. + * Duplicates are not allowed (by .equals comparison) + */ +public class RuleBook implements ReadOnlyRuleBook { + + private final UniqueRuleList rules; + + public RuleBook() { + rules = new UniqueRuleList(); + } + + public RuleBook(ReadOnlyRuleBook toBeCopied) { + rules = new UniqueRuleList(); + resetData(toBeCopied); + } + + //// list overwrite operations + + public void setRules(List rules) throws DuplicateRuleException { + this.rules.setRules(rules); + } + + /** + * Resets the existing data of this {@code RuleBook} with {@code newData}. + */ + public void resetData(ReadOnlyRuleBook newData) { + requireNonNull(newData); + List ruleList = newData.getRuleList(); + + try { + setRules(ruleList); + } catch (DuplicateRuleException e) { + throw new AssertionError("RuleBooks should not have duplicate rules"); + } + } + + //// rule-level operations + + /** + * Adds a rule to the address book. + * + * @throws DuplicateRuleException if an equivalent rule already exists. + */ + public void addRule(Rule rule) throws DuplicateRuleException { + rules.add(rule); + } + + /** + * Replaces the given rule {@code target} in the list with {@code editedRule}. + * + * @throws DuplicateRuleException if updating the rule's details causes the rule to be equivalent to + * another existing rule in the list. + * @throws RuleNotFoundException if {@code target} could not be found in the list. + */ + public void updateRule(Rule target, Rule editedRule) + throws DuplicateRuleException, RuleNotFoundException { + requireNonNull(editedRule); + rules.setRule(target, editedRule); + } + + /** + * Removes {@code key} from this {@code RuleBook}. + * @throws RuleNotFoundException if the {@code key} is not in this {@code RuleBook}. + */ + public boolean removeRule(Rule key) throws RuleNotFoundException { + if (rules.remove(key)) { + return true; + } else { + throw new RuleNotFoundException(); + } + } + + //// util methods + + @Override + public String toString() { + return rules.asObservableList().size() + " rules registered"; + } + + @Override + public ObservableList getRuleList() { + return rules.asObservableList(); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof RuleBook // instanceof handles nulls + && this.rules.equals(((RuleBook) other).rules)); + } + + @Override + public int hashCode() { + // use this method for custom fields hashing instead of implementing your own + return Objects.hash(rules); + } + +} +``` +###### \java\seedu\address\storage\XmlAdaptedRule.java +``` java +package seedu.address.storage; + +import javax.xml.bind.annotation.XmlElement; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.rule.NotificationRule; +import seedu.address.model.rule.Rule; +import seedu.address.model.rule.RuleType; + +/** + * JAXB-friendly version of the Rule. + */ +public class XmlAdaptedRule { + + public static final String MISSING_FIELD_MESSAGE_FORMAT = "Rule's %s field is missing!"; + + @XmlElement(required = true) + private String value; + + @XmlElement(required = true) + private String type; + + /** + * Constructs an XmlAdaptedRule. + * This is the no-arg constructor that is required by JAXB. + */ + public XmlAdaptedRule() {} + + /** + * Constructs an {@code XmlAdaptedRule} with the given rule details. + */ + public XmlAdaptedRule(String value, String type) { + this.value = value; + this.type = type; + } + + /** + * Converts a given Rule into this class for JAXB use. + * + * @param source future changes to this will not affect the created XmlAdaptedRule + */ + public XmlAdaptedRule(Rule source) { + value = source.description; + type = source.type.toString(); + } + + /** + * Converts this jaxb-friendly adapted rule object into the model's Rule object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted rule + */ + public Rule toModelType() throws IllegalValueException { + if (this.type == null) { + throw new IllegalValueException( + String.format(MISSING_FIELD_MESSAGE_FORMAT, RuleType.class.getSimpleName())); + } + if (this.value == null) { + throw new IllegalValueException( + String.format(MISSING_FIELD_MESSAGE_FORMAT, Rule.class.getSimpleName())); + } + + try { + switch (RuleType.valueOf(type)) { + case NOTIFICATION: + return new NotificationRule(value); + default: + throw new IllegalValueException(Rule.MESSAGE_RULE_INVALID); + } + } catch (IllegalArgumentException e) { + throw new IllegalValueException(Rule.MESSAGE_RULE_INVALID); + } + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof XmlAdaptedRule)) { + return false; + } + + XmlAdaptedRule otherRule = (XmlAdaptedRule) other; + return value.equals(otherRule.value); + } +} +``` +###### \java\seedu\address\ui\ChartsPanel.java +``` java +package seedu.address.ui; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +import com.google.common.eventbus.Subscribe; + +import javafx.fxml.FXML; +import javafx.scene.chart.CategoryAxis; +import javafx.scene.chart.LineChart; +import javafx.scene.chart.NumberAxis; +import javafx.scene.chart.XYChart.Data; +import javafx.scene.chart.XYChart.Series; +import javafx.scene.layout.Region; +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.events.ui.CoinPanelSelectionChangedEvent; +import seedu.address.model.coin.Amount; + +/** + * The charts panel used to display graphs + */ +public class ChartsPanel extends UiPart { + + public static final String FXML = "ChartsPanel.fxml"; + + private final Logger logger = LogsCenter.getLogger(ChartsPanel.class); + + @FXML + private CategoryAxis xAxis; + + @FXML + private NumberAxis yAxis; + + @FXML + private LineChart priceChart; + + public ChartsPanel() { + super(FXML); + + registerAsAnEventHandler(this); + } + + /** + * Adds a new plot to the graph via a coin price + * @param xAxis + * @param yAxis + */ + private void addPlot(List xAxis, List yAxis) { + ArrayList dateList = new ArrayList<>( + xAxis.stream() + .map(str -> new Date(parseTimeStamp(str))) + .collect(Collectors.toList())); + ArrayList priceList = new ArrayList<>( + yAxis.stream() + .map(amount -> Double.valueOf(amount.toString())) + .collect(Collectors.toList())); + + addPlot(dateList, priceList); + } + + /** + * Adds a new plot to the graph + */ + private void addPlot(ArrayList xAxis, ArrayList yAxis) { + Series dataSeries = new Series<>(); + populateData(dataSeries, xAxis, yAxis); + + priceChart.getData().add(dataSeries); + priceChart.setCreateSymbols(false); + + if (!xAxis.isEmpty()) { + calibrateRange(Collections.min(yAxis), Collections.max(yAxis), 5); + } + } + + private long parseTimeStamp(String s) { + return Long.valueOf(s + "000"); + } + + /** + * Sets nice values for the chart axis scaling + */ + private void calibrateRange(double min, double max, int steps) { + this.yAxis.setLowerBound(min); + this.yAxis.setUpperBound(max); + this.yAxis.setTickUnit((max - min) / (double) steps); + } + + /** + * Adds the data from the provided lists to the data series + * @param dataSeries + * @param xAxis + * @param yAxis + */ + private void populateData(Series dataSeries, ArrayList xAxis, ArrayList yAxis) { + assert (xAxis.size() == yAxis.size()); + for (int i = 0; i < xAxis.size(); i++) { + final String date = new SimpleDateFormat("dd MMM, HHmm").format(xAxis.get(i)); + dataSeries.getData().add(new Data<>(date, yAxis.get(i))); + } + } + + private void clearData() { + priceChart.getData().clear(); + } + + @Subscribe + private void handleCoinPanelSelectionChangedEvent(CoinPanelSelectionChangedEvent event) { + logger.info(LogsCenter.getEventHandlingLogMessage(event)); + clearData(); + addPlot(event.getNewSelection().coin.getPrice().getHistoricalTimeStamps(), + event.getNewSelection().coin.getPrice().getHistoricalPrices()); + } +} +``` +###### \java\seedu\address\ui\MainWindow.java +``` java + @Subscribe + private void handleShowNotifManEvent(ShowNotifManRequestEvent event) { + logger.info(LogsCenter.getEventHandlingLogMessage(event)); + notificationsWindow = new NotificationsWindow(secondaryStage, event.data); + notificationsWindow.show(); + } + + @Subscribe + private void handleShowNotificationEvent(ShowNotificationRequestEvent nre) { + logger.info(LogsCenter.getEventHandlingLogMessage(nre)); + spawnNotification(nre.toString(), nre.targetIndex, nre.codeString); + } + + /** + * Spawns a popup notification with the given message. + */ + private void spawnNotification(String message, Index index, String code) { + Notifications.create() + .title("The following rule has triggered this notification:") + .text(String.format("%1$s\nClick to jump to view %2$s", message, code)) + .graphic(new ImageView(IconUtil.getCoinIcon(code))) + .onAction(event -> { + try { + logic.execute(ListCommand.COMMAND_WORD); + EventsCenter.getInstance().post(new JumpToListRequestEvent(index)); + event.consume(); + } catch (Exception e) { + throw new RuntimeException(); + } + }) + .show(); + } +``` +###### \java\seedu\address\ui\NotificationsWindow.java +``` java +package seedu.address.ui; + +import java.util.logging.Logger; + +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.image.Image; +import javafx.scene.layout.StackPane; +import javafx.stage.Stage; +import seedu.address.commons.core.LogsCenter; +import seedu.address.model.rule.Rule; + +/** + * Controller for the notification manager + */ +public class NotificationsWindow extends UiPart { + + private static final Logger logger = LogsCenter.getLogger(BrowserWindow.class); + private static final Image WINDOW_ICON = new Image("/images/address_book_32.png"); + private static final String WINDOW_TITLE = "Notifications"; + private static final String FXML = "NotificationsWindow.fxml"; + + private RuleListPanel ruleListPanel; + + @FXML + private StackPane ruleListPanelPlaceholder; + + public NotificationsWindow(Stage stage, ObservableList data) { + super(FXML, stage); + + // Configure the UI + setTitle(); + setWindowDefaultSize(); + + registerAsAnEventHandler(this); + + ruleListPanel = new RuleListPanel(data); + ruleListPanelPlaceholder.getChildren().add(ruleListPanel.getRoot()); + } + + private void setTitle() { + this.getRoot().setTitle(WINDOW_TITLE); + } + + /** + * Sets the default size. + */ + private void setWindowDefaultSize() { + this.getRoot().setHeight(300); + this.getRoot().setWidth(500); + } + + /** + * Shows the notification window. + * @throws IllegalStateException + *
    + *
  • + * if this method is called on a thread other than the JavaFX Application Thread. + *
  • + *
  • + * if this method is called during animation or layout processing. + *
  • + *
  • + * if this method is called on the primary stage. + *
  • + *
  • + * if {@code dialogStage} is already showing. + *
  • + *
+ */ + public void show() { + logger.fine("Showing notification manager."); + getRoot().show(); + setWindowDefaultSize(); + } + +} +``` +###### \resources\view\ChartsPanel.fxml +``` fxml + + + + + + + + + + + + + + + + + +``` +###### \resources\view\Extensions.css +``` css +.chart { + -fx-horizontal-grid-lines-visible: false; + -fx-vertical-grid-lines-visible: false; + -fx-legend-visible: false; +} + +.chart-pane, .chart-plot-background { + -fx-background-color: #222222; +} + +.chart-series-line { + -fx-stroke-width: 1px !important; + -fx-effect: null; +} +``` +###### \resources\view\NotificationsWindow.fxml +``` fxml + + + + + + + + + + + + + + + + +``` diff --git a/collated/functional/laichengyu.md b/collated/functional/laichengyu.md new file mode 100644 index 000000000000..f120abe19f65 --- /dev/null +++ b/collated/functional/laichengyu.md @@ -0,0 +1,670 @@ +# laichengyu +###### \java\seedu\address\commons\events\ui\LoadingEvent.java +``` java + +package seedu.address.commons.events.ui; + +import seedu.address.commons.events.BaseEvent; + +/** + * An event to indicate that the app is loading data + */ +public class LoadingEvent extends BaseEvent { + + public final boolean isLoading; + + public LoadingEvent(boolean isLoading) { + this.isLoading = isLoading; + } + + @Override + public String toString() { + return this.getClass().getSimpleName(); + } + +} +``` +###### \java\seedu\address\commons\util\FetchUtil.java +``` java + +package seedu.address.commons.util; + +import static org.asynchttpclient.Dsl.asyncHttpClient; + +import java.io.FileNotFoundException; +import java.io.InputStreamReader; +import java.util.concurrent.Future; + +import org.asynchttpclient.AsyncHandler; +import org.asynchttpclient.AsyncHttpClient; +import org.asynchttpclient.BoundRequestBuilder; +import org.asynchttpclient.HttpResponseBodyPart; +import org.asynchttpclient.HttpResponseStatus; +import org.asynchttpclient.Response; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import io.netty.handler.codec.http.HttpHeaders; + +/** + * Retrieves data in JSON format from a specified URL + */ +public class FetchUtil { + + private static AsyncHttpClient myAsyncHttpClient = asyncHttpClient(); + + /** + * Returns a Future object, future from the specific url asynchronously. + * The HTTP request Response can be retrieved using future.get(). + * All operations queued before future.get() are performed async and the application + * will be thread-blocked at future.get() to wait for the return Response. + * @param url cannot be null + * @return a Future object that can retrieve a Response which contains HTTP request data + * in its responseBody + */ + public static Future asyncFetch(String url) { + //Send HTTP request asynchronously + BoundRequestBuilder boundReqBuilder = myAsyncHttpClient.prepareGet(url); + AsyncHandler asyncHandler = getResponseAsyncHandler(); + return boundReqBuilder.execute(asyncHandler); + } + + /** + * Creates a new async response handler + * @return AsyncHandler for Response objects + */ + private static AsyncHandler getResponseAsyncHandler() { + return new AsyncHandler() { + private Response.ResponseBuilder builder = new Response.ResponseBuilder(); + private Integer status; + @Override + public State onStatusReceived(HttpResponseStatus responseStatus) throws Exception { + status = responseStatus.getStatusCode(); + builder.accumulate(responseStatus); + if (status != 200) { + return State.ABORT; + } + return State.CONTINUE; + } + @Override + public State onHeadersReceived(HttpHeaders headers) throws Exception { + return State.CONTINUE; + } + @Override + public State onBodyPartReceived(HttpResponseBodyPart bodyPart) throws Exception { + builder.accumulate(bodyPart); + return State.CONTINUE; + } + @Override + public Response onCompleted() throws Exception { + return builder.build(); + } + @Override + public void onThrowable(Throwable t) { + } + }; + } + + /** + * Parses a String into a JsonObject + * @param str cannot be null + * @return JsonObject converted from String + */ + public static JsonObject parseStringToJsonObj(String str) { + JsonObject jsonObject; + + JsonParser parser = new JsonParser(); + JsonElement jsonElement = parser.parse(str); + jsonObject = jsonElement.getAsJsonObject(); + + return jsonObject; + } + + /** + * Parses a file at {@code filepath} as an array of JsonObjects + * @param fw cannot be null + * @return JsonArray that is contained in the file at {@code filepath} + */ + public static JsonArray parseFileToJsonObj(InputStreamReader fw) throws FileNotFoundException { + JsonObject jsonObject; + + JsonParser parser = new JsonParser(); + JsonElement jsonElement = parser.parse(fw); + return jsonElement.getAsJsonArray(); + } +} +``` +###### \java\seedu\address\commons\util\UrlBuilderUtil.java +``` java + +package seedu.address.commons.util; + +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.util.List; +import java.util.logging.Logger; + +import org.apache.http.NameValuePair; +import org.apache.http.client.utils.URIBuilder; + +import seedu.address.commons.core.LogsCenter; + +/** + * Builds a URL given a url and parameters + */ +public class UrlBuilderUtil { + + private static final Logger logger = LogsCenter.getLogger(UrlBuilderUtil.class); + + /** + * Builds a URL given the url and params + * @param url cannot be null + * @param params are necessary + * @return String URL concatenated with params + */ + public static String buildUrl(String url, List params) { + String parameterizedUrl = ""; + try { + URIBuilder uri = new URIBuilder(url); + uri.addParameters(params); + parameterizedUrl = uri.build().toURL().toString(); + } catch (URISyntaxException e) { + logger.info("Illegal characters found in url: " + url + " or params: " + params.toString()); + } catch (MalformedURLException e) { + logger.info("Malformed URL: " + url + " provided"); + } + return parameterizedUrl; + } +} +``` +###### \java\seedu\address\logic\CommandList.java +``` java + +package seedu.address.logic; + +import java.util.Arrays; +import java.util.List; + +import seedu.address.logic.commands.AddCommand; +import seedu.address.logic.commands.BuyCommand; +import seedu.address.logic.commands.ClearCommand; +import seedu.address.logic.commands.DeleteCommand; +import seedu.address.logic.commands.ExitCommand; +import seedu.address.logic.commands.FindCommand; +import seedu.address.logic.commands.HelpCommand; +import seedu.address.logic.commands.HistoryCommand; +import seedu.address.logic.commands.ListCommand; +import seedu.address.logic.commands.NotifyCommand; +import seedu.address.logic.commands.RedoCommand; +import seedu.address.logic.commands.SellCommand; +import seedu.address.logic.commands.SortCommand; +import seedu.address.logic.commands.SyncCommand; +import seedu.address.logic.commands.TagCommand; +import seedu.address.logic.commands.UndoCommand; +import seedu.address.logic.commands.ViewCommand; + +/** + * Stores a list of all available commands + */ +public class CommandList { + public static final List COMMAND_LIST = Arrays.asList(HelpCommand.COMMAND_WORD, AddCommand.COMMAND_WORD, + BuyCommand.COMMAND_WORD, SellCommand.COMMAND_WORD, DeleteCommand.COMMAND_WORD, + ClearCommand.COMMAND_WORD, TagCommand.COMMAND_WORD, ListCommand.COMMAND_WORD, + FindCommand.COMMAND_WORD, ViewCommand.COMMAND_WORD, NotifyCommand.COMMAND_WORD, + SortCommand.COMMAND_WORD, HistoryCommand.COMMAND_WORD, UndoCommand.COMMAND_WORD, + RedoCommand.COMMAND_WORD, SyncCommand.COMMAND_WORD, ExitCommand.COMMAND_WORD); + +} +``` +###### \java\seedu\address\logic\commands\SyncCommand.java +``` java + +package seedu.address.logic.commands; + +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +import org.apache.http.NameValuePair; +import org.apache.http.message.BasicNameValuePair; +import org.asynchttpclient.Response; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.reflect.TypeToken; + +import seedu.address.commons.core.EventsCenter; +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.events.ui.LoadingEvent; +import seedu.address.commons.util.FetchUtil; +import seedu.address.commons.util.UrlBuilderUtil; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.coin.Amount; +import seedu.address.model.coin.Price; +import seedu.address.model.coin.exceptions.CoinNotFoundException; +import seedu.address.model.coin.exceptions.DuplicateCoinException; + +/** + * Updates all coins in the coin book with latest cryptocurrency data + */ +public class SyncCommand extends Command { + + public static final String COMMAND_WORD = "sync"; + public static final String COMMAND_ALIAS = "sy"; + + public static final String MESSAGE_SUCCESS = "Synced all coins with latest cryptocurrency data"; + + private static final Logger logger = LogsCenter.getLogger(SyncCommand.class); + + private static final String historicalPriceApiUrl = "https://min-api.cryptocompare.com/data/histohour"; + private static final String cryptoCompareApiUrl = "https://min-api.cryptocompare.com/data/pricemultifull"; + + private static final String HISTORICAL = "historical"; + private static final String CURRENT = "current"; + + private static final String CODE_PARAM = "fsym"; + private static final String CURRENCY_PARAM = "tsym"; + private static final String PLURALIZE = "s"; + private static final String CURRENCY_TYPE = "USD"; + private static final String LIMIT_PARAM = "limit"; + private static final String HISTORICAL_DATA_HOURS_LIMIT = "168"; + + @Override + public CommandResult execute() throws CommandException { + try { + String commaSeparatedCodes = concatenateByComma(model.getCodeList()); + HashMap newPriceMetrics = createPriceObjects(getCurrentPriceRawData(commaSeparatedCodes)); + model.syncAll(newPriceMetrics); + } catch (DuplicateCoinException dpe) { + throw new CommandException("Unexpected code path!"); + } catch (CoinNotFoundException cnfe) { + throw new AssertionError("The target coin cannot be missing"); + } + return new CommandResult(MESSAGE_SUCCESS); + } + + /** + * Creates and returns a {@code List} with at least two key-value pairs, coin symbols and currency. + * Additional parameters are optional based on the {@code type} + * + * @param commaSeparatedCodes cannot be null + * @param type specifies type of data required + * @return parameters for specified API call + */ + private List buildParams(String commaSeparatedCodes, String type) { + List parameters = new ArrayList<>(); + addBasicNecessaryParams(parameters, commaSeparatedCodes, type); + addAdditionalParams(parameters, type); + return parameters; + } + + /** + * Add the API parameters to the given list + * + * @param params List of API parameters + * @param commaSeparatedCodes Coin codes to use + * @param type API type to get from + */ + private void addBasicNecessaryParams(List params, String commaSeparatedCodes, String type) { + switch (type) { + case HISTORICAL: + params.add(new BasicNameValuePair(CODE_PARAM, commaSeparatedCodes)); + params.add(new BasicNameValuePair(CURRENCY_PARAM, CURRENCY_TYPE)); + break; + case CURRENT: + params.add(new BasicNameValuePair(CODE_PARAM + PLURALIZE, commaSeparatedCodes)); + params.add(new BasicNameValuePair(CURRENCY_PARAM + PLURALIZE, CURRENCY_TYPE)); + break; + default: + break; + } + } + + /** + * Adds any additional parameters required for the API call + * + * @param params cannot be null + * @param type specifies type of data required + */ + void addAdditionalParams(List params, String type) { + switch (type) { + case HISTORICAL: + params.add(new BasicNameValuePair(LIMIT_PARAM, HISTORICAL_DATA_HOURS_LIMIT)); + break; + default: + //no additional parameters + } + } + + /** + * Concatenates a list of strings into one with each string separated by a comma + * + * @param list of strings to be concatenated + * @return comma separated string + */ + private String concatenateByComma(List list) { + return String.join(",", list); + } + + /** + * Adds parameters to the CryptoCompare API URL. + * + * @param params cannot be null + */ + private String buildApiUrl(String url, List params) { + return UrlBuilderUtil.buildUrl(url, params); + } + + /** + * Dispatches a {@code LoadingEvent} while waiting for the Response object from the Future object + * + * @param promise that returns the desired data + * @return Response object retrieved from the Future + * @throws InterruptedException + * @throws ExecutionException + */ + private Response waitForResponse(Future promise) throws InterruptedException, ExecutionException { + //Set loading UI + EventsCenter.getInstance().post(new LoadingEvent(true)); + Response response = promise.get(); + //Return UI to normal + EventsCenter.getInstance().post(new LoadingEvent(false)); + return response; + } + + /** + * Fetches the raw data for all codes current price. + */ + private JsonObject getCurrentPriceRawData(String commaSeparatedCodes) { + List currentPriceParams = buildParams(commaSeparatedCodes, CURRENT); + JsonElement currentPriceData = getJsonObject(cryptoCompareApiUrl, currentPriceParams).get("RAW"); + if (currentPriceData == null) { + return new JsonObject(); + } else { + return currentPriceData.getAsJsonObject(); + } + } + + /** + * Gets the specified object from the given web API call + * @param url API URL + * @param priceParams Parameters for API + */ + private JsonObject getJsonObject(String url, List priceParams) { + try { + String priceUrl = buildApiUrl(url, priceParams); + + Future promise = FetchUtil.asyncFetch(priceUrl); + Response response = waitForResponse(promise); + return FetchUtil.parseStringToJsonObj(response.getResponseBody()); + } catch (InterruptedException ie) { + logger.warning("Thread interrupted"); + } catch (ExecutionException ee) { + logger.warning("Data fetching error"); + } + return new JsonObject(); + } + + /** + * Creates and returns a {@code HashMap} of code and price metrics as key-value pairs. + * + * @param currentPriceData contains the latest prices of each of the user's coin + * @return HashMap containing price metrics of each coin retrieval by its code + */ + private HashMap createPriceObjects(JsonObject currentPriceData) { + requireAllNonNull(currentPriceData); + + HashMap priceObjs = new HashMap<>(); + List codes = model.getCodeList(); + + for (String code : codes) { + JsonElement coinCurrentPriceMetrics = currentPriceData.get(code); + + if (coinCurrentPriceMetrics == null) { + continue; + } + + Price newPrice = new Price(); + newPrice.setCurrent(new Amount(coinCurrentPriceMetrics + .getAsJsonObject() + .get(CURRENCY_TYPE) + .getAsJsonObject() + .get("PRICE") + .getAsString())); + +``` +###### \java\seedu\address\model\coin\Coin.java +``` java + /** + * Copy constructor with price update. + * Sets previous state to copied coin. + */ + public Coin(Coin toCopy, Price newPrice) { + requireAllNonNull(toCopy); + this.code = toCopy.code; + // protect internal tags from changes in the arg list + this.tags = new UniqueTagList(toCopy.getTags()); + this.price = new Price(newPrice); + this.totalAmountSold = new Amount(toCopy.getTotalAmountSold()); + this.totalAmountBought = new Amount(toCopy.getTotalAmountBought()); + this.totalDollarsSold = new Amount(toCopy.getTotalDollarsSold()); + this.totalDollarsBought = new Amount(toCopy.getTotalDollarsBought()); + prevState = toCopy; + } +``` +###### \java\seedu\address\model\coin\Price.java +``` java + /** + * Constructs a {@code Price} with given value. + */ + public Price(Price toCopy) { + this.currentPrice = toCopy.currentPrice; + this.historicalPrices = toCopy.historicalPrices; + this.historicalTimeStamps = toCopy.historicalTimeStamps; + } +``` +###### \java\seedu\address\model\CoinBook.java +``` java + /** + * Replaces every coin in the list that has a price change in {@code newData} with {@code updatedCoin}. + * {@code CoinBook}'s tag list will be updated with the tags of {@code updatedCoin}. + * + * @throws DuplicateCoinException if updating the coin's details causes the coin to be equivalent to + * another existing coin in the list. + * @throws CoinNotFoundException if {@code coin} could not be found in the list. + * + * @see #syncWithMasterTagList(Coin) + */ + public void syncAll(HashMap newPriceMetrics) + throws DuplicateCoinException, CoinNotFoundException { + requireNonNull(newPriceMetrics); + + for (Coin coin : coins) { + String code = coin.getCode().toString(); + Price newPrice = newPriceMetrics.get(code); + if (newPrice == null) { + continue; + } + Coin updatedCoin = new Coin(coin, newPrice); + updateCoin(coin, updatedCoin); + } + } +``` +###### \java\seedu\address\model\CoinBook.java +``` java + @Override + public List getCodeList() { + return Collections.unmodifiableList(coins.asObservableList().stream() + .map(coin -> coin.getCode().toString()) + .collect(Collectors.toList())); + } +``` +###### \java\seedu\address\model\Model.java +``` java + /** Returns an unmodifiable view of the code list */ + List getCodeList(); + + /** + * Syncs all coin data + */ + void syncAll(HashMap newPriceMetrics) + throws DuplicateCoinException, CoinNotFoundException; +``` +###### \java\seedu\address\model\ModelManager.java +``` java + @Override + public void syncAll(HashMap newPriceMetrics) + throws DuplicateCoinException, CoinNotFoundException { + requireNonNull(newPriceMetrics); + + coinBook.syncAll(newPriceMetrics); + indicateCoinBookChanged(); + } + + /** Returns an unmodifiable view of the code list */ + @Override + public List getCodeList() { + return coinBook.getCodeList(); + } +``` +###### \java\seedu\address\model\ReadOnlyCoinBook.java +``` java + /** + * Returns an unmodifiable view of the codes list. + * This list will not contain any duplicate codes. + */ + List getCodeList(); +``` +###### \java\seedu\address\storage\CoinBookStorage.java +``` java + /** + * Saves the given {@link ReadOnlyCoinBook} to a fixed temporary location. + * @param addressBook cannot be null. + * @throws IOException if there was any problem writing to the file. + */ + void backupCoinBook(ReadOnlyCoinBook addressBook) throws IOException; +``` +###### \java\seedu\address\storage\StorageManager.java +``` java + @Override + public void backupCoinBook(ReadOnlyCoinBook coinBook) throws IOException { + coinBookStorage.backupCoinBook(coinBook); + } +``` +###### \java\seedu\address\storage\XmlCoinBookStorage.java +``` java + @Override + public void backupCoinBook(ReadOnlyCoinBook addressBook) throws IOException { + saveCoinBook(addressBook, backupFilePath); + } +``` +###### \java\seedu\address\ui\CoinCard.java +``` java + @FXML + private ImageView icon; +``` +###### \java\seedu\address\ui\CoinCard.java +``` java + icon.setImage(IconUtil.getCoinIcon(coinCode)); +``` +###### \java\seedu\address\ui\CommandBox.java +``` java + SuggestionProvider suggestionProvider = SuggestionProvider.create(CommandList.COMMAND_LIST); + TextFields.bindAutoCompletion(commandTextField, suggestionProvider); +``` +###### \java\seedu\address\ui\IconUtil.java +``` java + public static final String ICON_BASE_FILE_PATH = "/images/coin_icons/"; + public static String getCoinFilePath(String code) { + return ICON_BASE_FILE_PATH + code + ".png"; + } + + public static Image getCoinIcon(String coinCode) { + try { + return AppUtil.getImage(getCoinFilePath(coinCode)); + } catch (NullPointerException e) { + return AppUtil.getImage(getCoinFilePath("empty")); + } + } +``` +###### \java\seedu\address\ui\MainWindow.java +``` java + private void setLoadingAnimation() { + ProgressIndicator pi = new ProgressIndicator(); + loadingAnimation = new VBox(pi); + loadingAnimation.setAlignment(Pos.CENTER); + } +``` +###### \java\seedu\address\ui\MainWindow.java +``` java + /** + * Displays loading animation when isLoading is true and hides it otherwise + * @param isLoading loading state of the application + */ + @FXML + private void handleLoading(boolean isLoading) { + toggleLoadingAnimation(isLoading); + } + + /** + * Adds or remove the loading animation from {@code coinListPanelPlaceholder} + * depending on the loading status + * @param isLoading the loading status of the application + */ + private void toggleLoadingAnimation(boolean isLoading) { + Platform.runLater(() -> { + if (isLoading) { + activateLoadingAnimation(); + } else { + deactivateLoadingAnimation(); + } + }); + } + + private void activateLoadingAnimation() { + loadingAnimation.setVisible(true); + coinListPanelPlaceholder.getChildren().add(loadingAnimation); + setTitle("Syncing..."); + } + + private void deactivateLoadingAnimation() { + loadingAnimation.setVisible(false); + coinListPanelPlaceholder.getChildren().remove(loadingAnimation); + setTitle(config.getAppTitle()); + } + + @Subscribe + private void handleLoadingEvent(LoadingEvent event) { + logger.info(LogsCenter.getEventHandlingLogMessage(event)); + handleLoading(event.isLoading); + } +``` +###### \resources\view\CoinListCard.fxml +``` fxml + + + + +