From 9a06f5a564bc66fa0cf9777aae78039825909c1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20Leutha=CC=88user?= <max.leuthaeuser@gmail.com> Date: Thu, 7 Jan 2016 15:53:35 +0100 Subject: [PATCH] - added task and tests --- README.md | 4 +- build.sbt | 2 +- src/main/scala/Main.scala | 55 +- src/main/scala/solution/Account.scala | 11 + src/main/scala/solution/Actor.scala | 9 - src/main/scala/solution/Bank.scala | 15 + src/main/scala/solution/Behavior.scala | 9 - src/main/scala/solution/Navigation.scala | 9 - src/main/scala/solution/Person.scala | 3 + src/main/scala/solution/Sensor.scala | 9 - src/main/scala/solution/Transaction.scala | 10 + src/main/scala/task/Accountable.scala | 3 + src/main/scala/task/Currency.scala | 970 ++++++++++++++++++++++ src/main/scala/task/Decreasable.scala | 5 + src/main/scala/task/Increasable.scala | 5 + src/main/scala/task/Result.scala | 12 - src/main/scala/task/Robot.scala | 3 - src/test/scala/BankTests.scala | 94 +++ src/test/scala/RobotTests.scala | 46 - 19 files changed, 1160 insertions(+), 114 deletions(-) create mode 100644 src/main/scala/solution/Account.scala delete mode 100644 src/main/scala/solution/Actor.scala create mode 100644 src/main/scala/solution/Bank.scala delete mode 100644 src/main/scala/solution/Behavior.scala delete mode 100644 src/main/scala/solution/Navigation.scala create mode 100644 src/main/scala/solution/Person.scala delete mode 100644 src/main/scala/solution/Sensor.scala create mode 100644 src/main/scala/solution/Transaction.scala create mode 100644 src/main/scala/task/Accountable.scala create mode 100644 src/main/scala/task/Currency.scala create mode 100644 src/main/scala/task/Decreasable.scala create mode 100644 src/main/scala/task/Increasable.scala delete mode 100644 src/main/scala/task/Result.scala delete mode 100644 src/main/scala/task/Robot.scala create mode 100644 src/test/scala/BankTests.scala delete mode 100644 src/test/scala/RobotTests.scala diff --git a/README.md b/README.md index 25586af..5342745 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ -Role-based Programming with SCROLL - Task 1 +Role-based Programming with SCROLL - Task 2 =========================================== -Task and test files for MOST SCROLL task 1. +Task and test files for MOST SCROLL task 2. **Edit and develop:** diff --git a/build.sbt b/build.sbt index c622d76..839c0a5 100644 --- a/build.sbt +++ b/build.sbt @@ -1,4 +1,4 @@ -name := "MOSTSCROLLTask1" +name := "MOSTSCROLLTask2" lazy val commonSettings = Seq( organization := "tu.dresden.de", diff --git a/src/main/scala/Main.scala b/src/main/scala/Main.scala index bfa3503..4737421 100644 --- a/src/main/scala/Main.scala +++ b/src/main/scala/Main.scala @@ -1,20 +1,47 @@ -import scroll.internal.Compartment -import solution.Behavior -import task.Robot -import solution.Behavior.ServiceRole -import solution.Navigation.NavigationRole -import solution.Sensor.ObservingRole -import solution.Actor.DrivableRole +import solution.{Transaction, Bank, Account, Person} +import task.Currency object Main extends App { - new Compartment { - // constructing a Robot with its roles - val myRobot = Robot("Pete") play ServiceRole() play NavigationRole() play ObservingRole() play DrivableRole() + val stan = Person("Stan") + val brian = Person("Brian") - // merging all the role-playing-relations defined above - Behavior partOf this + val accForStan = new Account(Currency(10.0, "USD")) + val accForBrian = new Account(Currency(0, "USD")) - // calling the actual behavior - println(myRobot move()) + new Bank { + stan play new Customer + brian play new Customer + accForStan play new CheckingsAccount + accForBrian play new SavingsAccount + + +stan addAccount accForStan + +brian addAccount accForBrian + + println("### Before transaction ###") + println("Balance for Stan: " + accForStan.balance) + println("Balance for Brian: " + accForBrian.balance) + + val transaction = new Transaction(Currency(10.0, "USD")) + accForStan play new transaction.Source + accForBrian play new transaction.Target + + // Defining a partOf relation between Transaction and Bank. + // The transaction needs full access to registered/bound Accounts like + // CheckingsAccount and SavingsAccount. + transaction partOf this + + transaction play new TransactionRole execute() + + println("### After transaction ###") + println("Balance for Stan: " + accForStan.balance) + println("Balance for Brian: " + accForBrian.balance) + + println("Stan is playing the Customer role? " + (+stan).isPlaying[Customer]) + println("Brian is playing the Customer role? " + (+brian).isPlaying[Customer]) + println("Account for Stan is a CheckingsAccount? " + (+accForStan).isPlaying[CheckingsAccount]) + println("Account for Brian is a SavingsAccount? " + (+accForBrian).isPlaying[SavingsAccount]) + + println(+stan listBalances()) + println(+brian listBalances()) } } \ No newline at end of file diff --git a/src/main/scala/solution/Account.scala b/src/main/scala/solution/Account.scala new file mode 100644 index 0000000..1cee6c9 --- /dev/null +++ b/src/main/scala/solution/Account.scala @@ -0,0 +1,11 @@ +package solution + +import task.Currency +import task.Increasable +import task.Decreasable + +// TODO: implement the class Account here! + +class Account /* ... */ { + /* ... */ +} \ No newline at end of file diff --git a/src/main/scala/solution/Actor.scala b/src/main/scala/solution/Actor.scala deleted file mode 100644 index 8ecd18a..0000000 --- a/src/main/scala/solution/Actor.scala +++ /dev/null @@ -1,9 +0,0 @@ -package solution - -import scroll.internal.Compartment - -object Actor extends Compartment { - - // TODO: Implement the role DrivableRole here! - -} diff --git a/src/main/scala/solution/Bank.scala b/src/main/scala/solution/Bank.scala new file mode 100644 index 0000000..6850644 --- /dev/null +++ b/src/main/scala/solution/Bank.scala @@ -0,0 +1,15 @@ +package solution + +import scroll.internal.support.DispatchQuery +import task.{Increasable, Decreasable, Accountable, Currency} +import scroll.internal.Compartment +import scroll.internal.support.DispatchQuery._ + +// TODO: implement the compartment Bank with its roles here! + +class Bank /* ... */ { + implicit var dd: DispatchQuery = DispatchQuery.empty + + /* ... */ + +} diff --git a/src/main/scala/solution/Behavior.scala b/src/main/scala/solution/Behavior.scala deleted file mode 100644 index 2854a95..0000000 --- a/src/main/scala/solution/Behavior.scala +++ /dev/null @@ -1,9 +0,0 @@ -package solution - -import scroll.internal.Compartment - -object Behavior extends Compartment { - - // TODO: Implement the role ServiceRole here! - -} diff --git a/src/main/scala/solution/Navigation.scala b/src/main/scala/solution/Navigation.scala deleted file mode 100644 index 82a0891..0000000 --- a/src/main/scala/solution/Navigation.scala +++ /dev/null @@ -1,9 +0,0 @@ -package solution - -import scroll.internal.Compartment - -object Navigation extends Compartment { - - // TODO: Implement the role NavigationRole here! - -} diff --git a/src/main/scala/solution/Person.scala b/src/main/scala/solution/Person.scala new file mode 100644 index 0000000..81ed6dc --- /dev/null +++ b/src/main/scala/solution/Person.scala @@ -0,0 +1,3 @@ +package solution + +// TODO: implement case class Person here! \ No newline at end of file diff --git a/src/main/scala/solution/Sensor.scala b/src/main/scala/solution/Sensor.scala deleted file mode 100644 index 93e4d8f..0000000 --- a/src/main/scala/solution/Sensor.scala +++ /dev/null @@ -1,9 +0,0 @@ -package solution - -import scroll.internal.Compartment - -object Sensor extends Compartment { - - // TODO: Implement the role ObservingRole here! - -} diff --git a/src/main/scala/solution/Transaction.scala b/src/main/scala/solution/Transaction.scala new file mode 100644 index 0000000..bf570ce --- /dev/null +++ b/src/main/scala/solution/Transaction.scala @@ -0,0 +1,10 @@ +package solution + +import scroll.internal.Compartment +import task.Currency + +// TODO: implement the compartment Transaction with its roles here! + +class Transaction /* ... */ { + /* ... */ +} \ No newline at end of file diff --git a/src/main/scala/task/Accountable.scala b/src/main/scala/task/Accountable.scala new file mode 100644 index 0000000..63ed426 --- /dev/null +++ b/src/main/scala/task/Accountable.scala @@ -0,0 +1,3 @@ +package task + +trait Accountable \ No newline at end of file diff --git a/src/main/scala/task/Currency.scala b/src/main/scala/task/Currency.scala new file mode 100644 index 0000000..5e5b47b --- /dev/null +++ b/src/main/scala/task/Currency.scala @@ -0,0 +1,970 @@ +/* + * Copyright (c) 2009 Thomas Knierim + * http://www.thomasknierim.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package task + +import java.math.{BigDecimal => BigDec} + +/** Represents an immutable, arbitrary-precision currency value with fixed point + * arithmetic. <p>A <code>Currency</code> value consists of a numeric value, + * an optional currency designation, and an optional number of decimal places. + * The default number of positions after the decimal point is 2 or respectively + * the number of decimal places commonly used for the specified currency.</p> + * + * <p>Its numeric value is internally represented by a 128-bit <code>java.math.BigDecimal</code> + * with 34 significant decimal places. This allows high-precision computations over + * a large range of values. As an added benefit of using <code>java.math.BigDecimal</code>, + * the <code>Currency</code> type gives users complete control over rounding behaviour. + * The default rounding more is <code>ROUND_HALF_UP</code> which is commonly used in + * commercial applications. The <code>ROUND_HALF_EVEN</code> mode may be useful for statistical + * applications.<p> + * + * <p>Computations are carried out with a precision of <i>n</i> positions after the decimal + * point, whereas <i>n</i> is specified by the <code>decimals</code> parameter. The position + * at <i>n + 1</i> is rounded according to the specified rounding mode.</p> + * + * <p>The <code>Currency</code> class distinguishes between specific and non-specific + * currencies. Specific currencies carry a designation that is specified by the respective + * ISO 4217 three-letter currency code. Non-specific values are values without currency code. + * Non-specific values can be added to or subtracted from specific values. Used with + * non-specific values only, the <code>Currency</code> class offers general purpose + * fixed-point arithmetic.</p> + * + * @author Thomas Knierim + * @version 1.0 + * + * 20090506 tk v first release + * 20100312 tk f fixed errors in country code map + * + */ +class Currency(value: BigDec, val currencyCode: String, val decimals: Int, + val roundingMode: Currency.RoundingMode.Value) extends Ordered[Currency] { + + if (decimals < 0) + throw new IllegalArgumentException("Currency decimal places must not be negative.") + + /** Contains the amount of the <code>Currency</code> value as + * <code>java.math.BigDecimal</code>. + */ + val amount = value.setScale(decimals, roundingMode.id) + + /** Returns a new <code>Currency</code> value with the specified number of decimals + * after the decimal point whose value is equal to <code>this</code> value. If the + * number of decimals is decreased, loss of precision may incur and the last position + * is rounded according to the specified round mode (<code>ROUND_HALF_UP</code> by + * default). + * + * @param d number of decimal places in the result value. + * @return this <code>Currency</code> value with <i>d</i> decimal places. + */ + def setDecimals(d: Int) = new Currency(amount, currencyCode, d, roundingMode) + + /** Compares this <code>Currency</code> value with the specified value for equality. + * Two currency values are considered equal if they have the same currency code + * and the same numeric value. Decimals and rounding mode are not regarded. + * Note that this means that equal currency values can have different string + * representations, e.g. <code>"10 USD" == "10.00 USD"</code>. + * + * @param that <code>Currency</code> value to comapare to <code>this</code> value. + * @return <code>true</code> if the values are equal, <code>false</code> otherwise. + */ + override def equals(that: Any): Boolean = that match { + case that: Currency => this.currencyCode == that.currencyCode && + this.amount.compareTo(that.amount) == 0 + case _ => false + } + + /* Trims trailing zeros from decimal positions of a numeric String + */ + private def trimZeros(s: String) = { + if (s.indexOf('.') > -1) { + var i = s.length + while (i > 0 && s(i - 1) == '0') i -= 1 + if (s(i - 1) == '.') i -= 1 + s.substring(0, i) + } + else s + } + + /** Returns a hash code for this <code>Currency</code> value. + * + * @return hash code for <code>this</code> value. + */ + override def hashCode: Int = + 41 * (41 + trimZeros(amount.toString).hashCode) + currencyCode.hashCode + + /** Compares this <code>Currency</code> value with the specified value. + * Throws MismatchedCurrencyException if two values with + * different currency codes are compared. + * + * @param that <code>Currency</code> value to which <code>this</code> value is compared. + * @return -1, 0, or 1 as this <code>Currency</code> value is less than, equal to, or + * greater than <code>that</code>. + * @throws MismatchedCurrencyException if currency codes don't match. + */ + def compare(that: Currency): Int = + if (this.currencyCode != that.currencyCode) + throw new MismatchedCurrencyException + else + this.amount.compareTo(that.amount) + + /** Returns the smaller of this <code>Currency</code> value and the given value. + * If both values are equal, <code>this</code> is returned. + * + * @param that <code>Currency</code> value to compare to. + * @return the smaller <code>Currency</code> value. + * @throws MismatchedCurrencyException if currency codes don't match. + */ + def min(that: Currency): Currency = + if (this.compare(that) <= 0) this else that + + /** Returns the larger of this <code>Currency</code> value and the given value. + * If both values are equal, <code>this</code> is returned. + * + * @param that <code>Currency</code> value to compare to. + * @return the larger <code>Currency</code> value. + * @throws MismatchedCurrencyException if currency codes don't match. + */ + def max(that: Currency): Currency = + if (this.compare(that) >= 0) this else that + + /** Adds a <code>Currency</code> value to this value and returns the sum as a new + * <code>Currency</code> value. The number of decimal places in the result is + * automatically adjusted to that of the summand with the wider scale. Currency + * codes need to be regarded: + * <ul> + * <li><code>55.55 EUR + 33.33 EUR = 88.88 EUR></code></li> + * <li><code>55.55 EUR + 33.33 = 88.88 EUR></code></li> + * <li><code>55.55 EUR + 33.33 USD = MismatchedCurrencyException</code></li> + * <li><code>55.5555 EUR + 33.33 EUR = 88.8855 EUR</code></li> + * </ul> + * + * @param that <code>Currency</code> value to add. + * @return the sum of both <code>Currency</code> values. + * @throws MismatchedCurrencyException if currency codes don't match. + * + */ + def +(that: Currency): Currency = { + var code = this.currencyCode + if (code.isEmpty) + code = that.currencyCode + else if (this.currencyCode != that.currencyCode && !that.currencyCode.isEmpty) + throw new MismatchedCurrencyException + val result = this.amount.add(that.amount) + new Currency(result, code, result.scale, this.roundingMode) + } + + /** Subtracts a <code>Currency</code> value from this value and returns the difference + * as a new <code>Currency</code> value. The number of decimal places in the result is + * automatically adjusted to that of the argument with the wider scale. Currency + * codes need to be regarded: + * <ul> + * <li><code>55.55 EUR - 33.33 EUR = <b>22.22 EUR</b></code></li> + * <li><code>55.55 EUR - 33.33 = <b>22.22 EUR</b></code></li> + * <li><code>55.55 EUR - 33.33 USD = <b>MismatchedCurrencyException</b></code></li> + * <li><code>55.5555 EUR - 33.33 EUR = <b>22.2255 EUR</b></code></li> + * </ul> + * + * @param that <code>Currency</code> value to subtract. + * @return the value of <code>this - that</code>. + * @throws MismatchedCurrencyException if currency codes don't match. + */ + def -(that: Currency): Currency = { + var code = this.currencyCode + if (code.isEmpty) + code = that.currencyCode + else if (this.currencyCode != that.currencyCode && !that.currencyCode.isEmpty) + throw new MismatchedCurrencyException + val result = this.amount.subtract(that.amount) + new Currency(result, code, result.scale, this.roundingMode) + } + + /** Multiplies this <code>Currency</code> value with a <code>java.math.BigDecimal</code> + * scalar value and returns a new <code>Currency</code> value. + * + * @param that the mulitiplier. + * @return the value of <code>this * that</code>. + */ + def *(that: BigDec): Currency = + new Currency(this.amount.multiply(that), currencyCode, decimals, roundingMode) + + /** Multiplies this <code>Currency</code> value with a <code>Double</code> + * scalar value and returns a new <code>Currency</code> value. + * + * @param that the mulitiplier. + * @return the value of <code>this * that</code>. + */ + def *(that: Double): Currency = this * BigDec.valueOf(that) + + /** Multiplies this <code>Currency</code> value with a <code>Long</code> + * scalar value and returns a new <code>Currency</code> value. + * + * @param that the mulitiplier. + * @return the value of <code>this * that</code>. + */ + def *(that: Long): Currency = this * BigDec.valueOf(that) + + /** Divides this <code>Currency</code> value by a <code>java.math.BigDecimal</code> + * scalar value and returns a new <code>Currency</code> value. + * + * @param that the divisor. + * @return the value of <code>this / that</code>. + */ + def /(that: BigDec): Currency = + new Currency(this.amount.divide(that, roundingMode.id), currencyCode, decimals, roundingMode) + + /** Divides this <code>Currency</code> value by a <code>Double</code> + * scalar value and returns a new <code>Currency</code> value. + * + * @param that the divisor. + * @return the value of <code>this / that</code>. + */ + def /(that: Double): Currency = this / BigDec.valueOf(that) + + /** Divides this <code>Currency</code> value by a <code>Long</code> + * scalar value and returns a new <code>Currency</code> value. + * + * @param that the divisor. + * @return the value of <code>this / that</code>. + */ + def /(that: Long): Currency = this / BigDec.valueOf(that) + + /** Returns a <code>Currency</code> whose value is <code>-this</code>. + * + * @return -this. + */ + def unary_- : Currency = new Currency(amount.negate(), currencyCode, decimals, roundingMode) + + /** Returns a <code>Currency</code> whose value is the absolute value of this + * <code>Currency</code>. + * + * @return abs(this). + * + */ + def abs: Currency = new Currency(amount.abs(), currencyCode, decimals, roundingMode) + + /** Returns a <code>Currency</code> whose value is <code>this * p / 100</code>. + * + * @param p the percentage. + * @return the result of the applied percentage. + */ + def percent(p: Double) = (this * p) / 100 + + /** Returns a <code>Currency</code> whose value is the integer part of this value. + * Example: <code>Currency(2.50).integral</code> returns <code>Currency(2.00)</code>. + * + * @return the integral part of this value. + */ + def integral = new Currency(amount.divideToIntegralValue(new BigDec("1")), + currencyCode, decimals, roundingMode) + + /** Returns a <code>Currency</code> whose value is the fractional part of this value. + * Example: <code>Currency(2.50).fraction</code> returns <code>Currency(0.50)</code>. + * + * @return the fractionalal part of this value. + */ + def fraction = new Currency(amount.subtract(integral.amount), + currencyCode, decimals, roundingMode) + + /** Returns a <code>Currency</code> whose value is <code>(this<sup>exp</sup>). + * + * @param exp the power to raise this <code>Currency</code> value to. + * @return this<sup>exp</sup>. + */ + def pow(exp: Int) = new Currency(amount.pow(exp), currencyCode, decimals, roundingMode) + + /** Returns a canonical <code>String</code> representation of the <code>Currency</code> + * value with 3-letter ISO currency code. + * + * @return formatted String with <code>decimals</code> positions after decimal point. + */ + override def toString = { + amount.toString + + (if (currencyCode.isEmpty) "" else " " + currencyCode) + } + + /** Returns the amount of the <code>Currency</code> value as <code>Double</code> value. + * If this value has too great a magnitude represent as a <code>Double</code>, it will + * be converted to <code>Double.NEGATIVE_INFINITY</code> or + * <code>Double.POSITIVE_INFINITY</code> as appropriate. + * + * @return this <code>Currency</code> value converted to a <code>Double</code>. + */ + def toDouble = amount.doubleValue + + /** Returns the amount of the <code>Currency</code> value as <code>Float</code> value. + * If this value has too great a magnitude represent as a <code>Float</code>, it will + * be converted to <code>Float.NEGATIVE_INFINITY</code> or + * <code>Float.POSITIVE_INFINITY</code> as appropriate. + * + * @return this <code>Currency</code> value converted to a <code>Float</code>. + */ + def toFloat = amount.floatValue + + /** Returns the amount of the <code>Currency</code> value as <code>Long</code> value. + * Any fractional part will be discarded. If this value is too big to fit in a + * <code>Long</code>, only the low-order 64 bits are returned. + * + * @return this <code>Currency</code> value converted to a <code>Long</code>. + */ + def toLong = amount.longValue + + /** Returns the amount of the <code>Currency</code> value as an <code>Int</code> value. + * Any fractional part will be discarded. If this value is too big to fit in an + * <code>Int</code>, only the low-order 32 bits are returned. + * + * @return this <code>Currency</code> value converted to an <code>Int</code>. + */ + def toInt = amount.intValue + + /** Returns the currency symbol for this <code>Currency</code>, or the empty + * string if this is a non-specific <code>Currency</code> value. + * + * @return Unicode currency symbol. + * + */ + def symbol = { + if (currencyCode.isEmpty) + "" + else { + var symbol = Currency.getSymbolFor(this.currencyCode) + if (symbol.isEmpty) + currencyCode + else + symbol + } + } + + /** Returns the English name for this <code>Currency</code>, or the empty + * string if this is a non-specific <code>Currency</code> value. + * + * @return currency name. + */ + def name = Currency.getNameFor(this.currencyCode) + + /** Formats a currency value according to default locale. + * See <code>java.util.Locale</code> and <code>java.text.NumberFormat</code> for details. + */ + def format: String = { + val nf = java.text.NumberFormat.getCurrencyInstance + nf.setCurrency(java.util.Currency.getInstance("USD")) + nf.format(amount).replace("USD", symbol).replace("$", symbol) + } + + /** Formats a currency value according to given locale. + * See <code>java.util.Locale</code> and <code>java.text.NumberFormat</code> for details. + */ + def format(locale: java.util.Locale): String = { + val nf = java.text.NumberFormat.getCurrencyInstance(locale) + nf.setCurrency(java.util.Currency.getInstance("USD")) + nf.format(amount).replace("USD", symbol).replace("$", symbol) + } + + /** Formats a currency value according to the given pattern. + * See <code>java.text.DecimalFormat</code> for details on format patterns. + * + * @param pattern a decimal number pattern string. + * @throws IllegalArgumentException if the given pattern is invalid. + */ + def format(pattern: String): String = { + new java.text.DecimalFormat(pattern. + replace("\u00A4\u00A4", currencyCode). + replace("\u00A4", symbol)). + format(amount) + } + + /** Formats a currency value according to the given pattern and with the + * given decimal symbols. See <code>java.text.DecimalFormat</code> for + * details on format patterns and symbols. + * + * @param pattern a decimal number pattern string. + * @param symbols the set of symbols to be used. + * @throws IllegalArgumentException if the given pattern is invalid. + */ + def format(pattern: String, symbols: java.text.DecimalFormatSymbols): String = { + new java.text.DecimalFormat(pattern. + replace("\u00A4\u00A4", currencyCode). + replace("\u00A4", symbol), symbols). + format(amount) + } + + /** Formats a currency value according to the given <code>DecimalFormat</code> object. + * This method gives you the most fine-grained control over the format output. It is + * also the most efficient one for repetitive formatting of <code>Currency</code> values. + * See <code>java.text.DecimalFormat</code> for details. + * + * @param format custom format object. + * @throws IllegalArgumentException if the given custom format cannot be applied. + */ + def format(format: java.text.DecimalFormat): String = format.format(amount) +} + +/** This exception is thrown if an arithmetic operation is attempted on + * two values with currencies that don't match. + */ +class MismatchedCurrencyException extends Exception + +/** This exception is thrown if a currency with an unknown ISO ISO 4217 currency + * code is created or requested. + */ +class UnknownCurrencyException extends Exception + +object Currency { + + object RoundingMode extends Enumeration { + type RoundingMode = Value + + /** Rounding mode to round away from zero. Always increments the digit prior to + * a nonzero discarded fraction. Note that this rounding mode never decreases + * the magnitude of the calculated value. + */ + val ROUND_UP = Value + + /** Rounding mode to round towards zero. Never increments the digit prior to a + * discarded fraction (i.e., truncates). Note that this rounding mode never + * increases the magnitude of the calculated value. + */ + val ROUND_DOWN = Value + + /** Rounding mode to round towards positive infinity. If the <code>Currency</code> + * value is positive, behaves as for ROUND_UP; if negative, behaves as for + * ROUND_DOWN. Note that this rounding mode never decreases the calculated value. + */ + val ROUND_CEILING = Value + + /** Rounding mode to round towards negative infinity. If the <code>Currency</code> + * value is positive, behave as for ROUND_DOWN; if negative, behave as for + * ROUND_UP. Note that this rounding mode never increases the calculated value. + */ + val ROUND_FLOOR = Value + + /** Rounding mode to round towards "nearest neighbor" unless both neighbors + * are equidistant, in which case round up. Behaves as for ROUND_UP if the + * discarded fraction is >= 0.5; otherwise, behaves as for ROUND_DOWN. Note + * that this is the rounding mode that most of us were taught in grade school. + * <p>Also known as common rounding.</p> + */ + val ROUND_HALF_UP = Value + + /** Rounding mode to round towards "nearest neighbor" unless both neighbors are + * equidistant, in which case round down. Behaves as for ROUND_UP if the discarded + * fraction is > 0.5; otherwise, behaves as for ROUND_DOWN. + */ + val ROUND_HALF_DOWN = Value + + /** Rounding mode to round towards the "nearest neighbor" unless both neighbors + * are equidistant, in which case, round towards the even neighbor. Behaves as + * for ROUND_HALF_UP if the digit to the left of the discarded fraction is odd; + * behaves as for ROUND_HALF_DOWN if it's even. Note that this is the rounding + * mode that minimizes cumulative error when applied repeatedly over a sequence + * of calculations. <p>Also known as Banker's Rounding.</p> + */ + val ROUND_HALF_EVEN = Value + + /** Rounding mode to assert that the requested operation has an exact result, + * hence no rounding is necessary. If this rounding mode is specified on an + * operation that yields an inexact result, an ArithmeticException is thrown. + */ + val ROUND_UNNECESSARY = Value + } + + import Currency.RoundingMode._ + + /* ----- CURRENCY DATA ----- */ + + private val currencies = Map( + "AED" ->("", 784, 2, "Fils", "UAE Dirham", "AE"), + "AFN" ->("", 971, 2, "Pul", "Afghani", "AF"), + "ALL" ->("L", 8, 2, "Qintar", "Lek", "AL"), + "AMD" ->("դր", 51, 0, "Luma", "Armenian Dram", "AM"), + "ANG" ->("ƒ", 532, 2, "Cent", "Netherlands Antillean Guilder", "AN"), + "AOA" ->("Kz", 973, 1, "Cêntimo", "Kwanza", "AO"), + "ARS" ->("$", 32, 2, "Centavo", "Argentine Peso", "AR"), + "AUD" ->("$", 36, 2, "Cent", "Australian Dollar", "AU"), + "AWG" ->("ƒ", 533, 2, "Cent", "Aruban Florin", "AW"), + "AZN" ->("", 944, 2, "Qəpik", "Azerbaijanian Manat", "AZ"), + "BAM" ->("КМ", 977, 2, "Fening", "Convertible Marks", "BA"), + "BBD" ->("$", 52, 2, "Cent", "Barbados Dollar", "BB"), + "BDT" ->("৳", 50, 2, "Paisa", "Bangladeshi Taka", "BD"), + "BGN" ->("лв", 975, 2, "Stotinka", "Bulgarian Lev", "BG"), + "BHD" ->("", 48, 3, "Fils", "Bahraini Dinar", "BH"), + "BIF" ->("Fr", 108, 0, "Centime", "Burundian Franc", "BI"), + "BMD" ->("$", 60, 2, "Cent", "Bermuda Dollar", "BM"), + "BND" ->("$", 96, 2, "Sen", "Brunei Dollar", "BN"), + "BOB" ->("Bs.", 68, 2, "Centavo", "Boliviano", "BO"), + "BRL" ->("R$", 986, 2, "Centavo", "Brazilian Real", "BR"), + "BSD" ->("$", 44, 2, "Cent", "Bahamian Dollar", "BS"), + "BTN" ->("", 64, 2, "Chertrum", "Ngultrum", "BT"), + "BWP" ->("P", 72, 2, "Thebe", "Pula", "BW"), + "BYR" ->("Br", 974, 0, "Kapyeyka", "Belarussian Ruble", "BY"), + "BZD" ->("$", 84, 2, "Cent", "Belize Dollar", "BZ"), + "CAD" ->("$", 124, 2, "Cent", "Canadian Dollar", "CA"), + "CDF" ->("Fr", 976, 2, "Centime", "Franc Congolais", "CD"), + "CHF" ->("Fr", 756, 2, "Rappen", "Swiss Franc", "CH"), + "CLP" ->("$", 152, 0, "Centavo", "Chilean Peso", "CL"), + "CNY" ->("¥", 156, 1, "Fen", "Yuan", "CN"), + "COP" ->("$", 170, 0, "Centavo", "Colombian Peso", "CO"), + "CRC" ->("₡", 188, 2, "Céntimo", "Costa Rican Colon", "CR"), + "CUP" ->("$", 192, 2, "Centavo", "Cuban Peso", "CU"), + "CVE" ->("Esc", 132, 2, "Centavo", "Cape Verde Escudo", "CV"), + "CZK" ->("Kč", 203, 2, "Haléř", "Czech Koruna", "CZ"), + "DJF" ->("Fr", 262, 0, "Centime", "Djibouti Franc", "DJ"), + "DKK" ->("kr", 208, 2, "Øre", "Danish Krone", "DK"), + "DOP" ->("$", 214, 2, "Centavo", "Dominican Peso", "DO"), + "DZD" ->("", 12, 2, "Centime", "Algerian Dinar", "DZ"), + "EEK" ->("KR", 233, 2, "Sent", "Kroon", "EE"), + "EGP" ->("£", 818, 2, "Piastre", "Egyptian Pound", "EG"), + "ERN" ->("Nfk", 232, 2, "Cent", "Nakfa", "ER"), + "ETB" ->("", 230, 2, "Santim", "Ethiopian Birr", "ET"), + "EUR" ->("€", 978, 2, "Cent", "Euro", "AT,BE,CY,ES,FI,FR,DE,GR,IE,IT,LU,MT,NL,PT,SI,SK"), + "FJD" ->("$", 242, 2, "Cent", "Fiji Dollar", "FJ"), + "FKP" ->("£", 238, 2, "Penny", "Falkland Islands Pound", "FK"), + "GBP" ->("£", 826, 2, "Penny", "Pound Sterling", "UK"), + "GEL" ->("ლ", 981, 2, "Tetri", "Lari", "GE"), + "GHS" ->("₵", 936, 2, "Pesewa", "Cedi", "GH"), + "GIP" ->("£", 292, 2, "Penny", "Gibraltar Pound", "GI"), + "GMD" ->("D", 270, 2, "Butut", "Dalasi", "GM"), + "GNF" ->("Fr", 324, 0, "Centime", "Guinea Franc", "GQ"), + "GTQ" ->("Q", 320, 2, "Centavo", "Quetzal", "GT"), + "GYD" ->("$", 328, 2, "Cent", "Guyana Dollar", "GY"), + "HKD" ->("$", 344, 1, "cent", "Hong Kong Dollar", "HK"), + "HNL" ->("L", 340, 2, "Centavo", "Lempira", "HN"), + "HRK" ->("kn", 191, 2, "Lipa", "Croatian Kuna", "HR"), + "HTG" ->("G", 332, 2, "Centime", "Haiti Gourde", "HT"), + "HUF" ->("Ft", 348, 0, "Penny", "Fillér", "HU"), + "IDR" ->("Rp", 360, 0, "Sen", "Rupiah", "ID"), + "ILS" ->("₪", 376, 2, "Agora", "Israeli New Sheqel", "IL"), + "INR" ->("₨", 356, 2, "Penny", "Paisa", "IN"), + "IQD" ->("", 368, 0, "Fils", "Iraqi Dinar", "IQ"), + "IRR" ->("", 364, 0, "Dinar", "Iranian Rial", "IR"), + "ISK" ->("kr", 352, 0, "Eyrir", "Iceland Krona", "IS"), + "JMD" ->("$", 388, 2, "Cent", "Jamaican Dollar", "JM"), + "JOD" ->("", 400, 3, "Piastre", "Jordanian Dinar", "JO"), + "JPY" ->("¥", 392, 0, "Sen", "Japanese Yen", "JP"), + "KES" ->("Sh", 404, 2, "Cent", "Kenyan Shilling", "KE"), + "KGS" ->("", 417, 2, "Tyiyn", "Som", "KG"), + "KHR" ->("", 116, 0, "Sen", "Riel", "KH"), + "KMF" ->("Fr", 174, 0, "Centime", "Comoro Franc", "KM"), + "KPW" ->("₩", 408, 0, "Chŏn", "North Korean Won", "KP"), + "KRW" ->("₩", 410, 0, "Jeon", "South Korean Won", "KR"), + "KWD" ->("", 414, 3, "Fils", "Kuwaiti Dinar", "KW"), + "KYD" ->("$", 136, 2, "Cent", "Cayman Islands Dollar", "KY"), + "KZT" ->("〒", 398, 2, "Tiyn", "Tenge", "KZ"), + "LAK" ->("₭", 418, 0, "Att", "Kip", "LA"), + "LBP" ->("", 422, 0, "Piastre", "Lebanese Pound", "LB"), + "LKR" ->("Rs", 144, 2, "Cent", "Sri Lanka Rupee", "LK"), + "LRD" ->("$", 430, 2, "Cent", "Liberian Dollar", "LR"), + "LSL" ->("L", 426, 2, "Sente", "Lesotho Loti", "LS"), + "LTL" ->("Lt", 440, 2, "Centas", "Lithuanian Litas", "LT"), + "LVL" ->("Ls", 428, 2, "Santīms", "Latvian Lats", "LV"), + "LYD" ->("", 434, 3, "Dirham", "Libyan Dinar", "LY"), + "MAD" ->("", 504, 2, "Centime", "Moroccan Dirham", "MA"), + "MDL" ->("L", 498, 2, "Ban", "Moldovan Leu", "MD"), + "MGA" ->("", 969, 1, "Iraimbilanja", "Malagasy Ariary", "MG"), + "MKD" ->("ден", 807, 2, "Deni", "Denar", "MK"), + "MMK" ->("K", 104, 0, "Pya", "Kyat", "MM"), + "MNT" ->("₮", 496, 2, "Möngö", "Tugrik", "MN"), + "MOP" ->("", 446, 1, "Penny", "Pataca", "MO"), + "MRO" ->("P", 478, 1, "Avo", "Ouguiya", "MR"), + "MUR" ->("₨", 480, 2, "Cent", "Mauritius Rupee", "MU"), + "MVR" ->("", 462, 2, "Lari", "Rufiyaa", "MV"), + "MWK" ->("MK", 454, 2, "Tambala", "Kwacha", "MW"), + "MXN" ->("$", 484, 2, "Centavo", "Mexican Peso", "MX"), + "MYR" ->("RM", 458, 2, "Sen", "Malaysian Ringgit", "MY"), + "MZN" ->("MTn", 943, 2, "Centavo", "Metical", "MZ"), + "NAD" ->("$", 516, 2, "Cent", "Namibian Dollar", "NA"), + "NGN" ->("₦", 566, 2, "Kobo", "Naira", "NG"), + "NIO" ->("C$", 558, 2, "Centavo", "Cordoba Oro", "NI"), + "NOK" ->("kr", 578, 2, "Øre", "Norwegian Krone", "NO"), + "NPR" ->("₨", 524, 2, "Paisa", "Nepalese Rupee", "NP"), + "NZD" ->("$", 554, 2, "Cent", "New Zealand Dollar", "NZ"), + "OMR" ->("", 512, 3, "Baisa", "Rial Omani", "OM"), + "PAB" ->("B/.", 590, 2, "Centésimo", "Balboa", "PA"), + "PEN" ->("S/.", 604, 2, "Céntimo", "Nuevo Sol", "PE"), + "PGK" ->("K", 598, 2, "Toea", "Kina Papua", "PG"), + "PHP" ->("₱", 608, 2, "Centavo", "Philippine Peso", "PH"), + "PKR" ->("₨", 586, 2, "Paisa", "Pakistan Rupee", "PK"), + "PLN" ->("zł", 985, 2, "Grosz", "Złoty", "PL"), + "PYG" ->("₲", 600, 0, "Céntimo", "Guarani", "PY"), + "QAR" ->("", 634, 2, "Dirham", "Qatari Rial", "QA"), + "RON" ->("L", 946, 2, "Ban", "Romanian Leu", "RO"), + "RSD" ->("din.", 941, 2, "Para", "Serbian Dinar", "RS"), + "RUB" ->("р.", 643, 2, "Kopek", "Russian Rouble", "RU"), + "RWF" ->("Fr", 646, 0, "Centime", "Rwanda Franc", "RW"), + "SAR" ->("", 682, 2, "Hallallah", "Saudi Riyal", "SA"), + "SBD" ->("$", 90, 2, "Cent", "Solomon Islands Dollar", "SB"), + "SCR" ->("₨", 690, 2, "Cent", "Seychelles Rupee", "SC"), + "SDG" ->("£", 938, 2, "Piastre", "Sudanese Pound", "SD"), + "SEK" ->("kr", 752, 2, "Öre", "Swedish Krona", "SE"), + "SGD" ->("$", 702, 2, "Cent", "Singapore Dollar", "SG"), + "SHP" ->("£", 654, 2, "Penny", "Saint Helena Pound", "SH"), + "SLL" ->("Le", 694, 0, "Cent", "Leone", "SL"), + "SOS" ->("Sh", 706, 2, "Cent", "Somali Shilling", "SO"), + "SRD" ->("$", 968, 2, "Cent", "Surinam Dollar", "SR"), + "STD" ->("Db", 678, 0, "Cêntimo", "Dobra", "ST"), + "SYP" ->("£", 760, 2, "Piastre", "Syrian Pound", "SY"), + "SZL" ->("L", 748, 2, "Cent", "Lilangeni", "SZ"), + "THB" ->("฿", 764, 2, "Satang", "Baht", "TH"), + "TJS" ->("ЅМ", 972, 2, "Diram", "Somoni", "TJ"), + "TMM" ->("m", 934, 2, "Tennesi", "Manat", "TM"), + "TND" ->("", 788, 3, "Millime", "Tunisian Dinar", "TN"), + "TOP" ->("T$", 776, 2, "Seniti", "Pa'anga", "TO"), + "TRY" ->("₤", 949, 2, "kuruş", "Turkish Lira", "TR"), + "TTD" ->("$", 780, 2, "Cent", "Trinidad and Tobago Dollar", "TT"), + "TWD" ->("$", 901, 1, "Cent", "New Taiwan Dollar", "TW"), + "TZS" ->("Sh", 834, 2, "Cent", "Tanzanian Shilling", "TZ"), + "UAH" ->("₴", 980, 2, "Kopiyka", "Hryvnia", "UA"), + "UGX" ->("Sh", 800, 0, "Cent", "Uganda Shilling", "UG"), + "USD" ->("$", 840, 2, "Cent", "Dollar", "US"), + "UYU" ->("$", 858, 2, "Centésimo", "Peso Uruguayo", "UY"), + "UZS" ->("", 860, 2, "Tiyin", "Uzbekistan Som", "UZ"), + "VEF" ->("Bs F", 937, 2, "Céntimo", "Venezuelan Bolívar", "VE"), + "VND" ->("₫", 704, 0, "Hào", "Vietnamese Dồng", "VN"), + "VUV" ->("Vt", 548, 0, "", "Vatu", "VU"), + "WST" ->("T", 882, 2, "Sene", "Samoan Tala", "WS"), + "XAF" ->("Fr", 950, 0, "Centime", "Central African CFA Franc", "CM,CF,CD,GQ,GA,TD"), + "XCD" ->("$", 951, 2, "Cent", "East Caribbean Dollar", "AG,DM,GD,KN,LC,VC"), + "XOF" ->("Fr", 952, 0, "Centime", "CFA Franc BCEAO", "BJ,BF,CI,GW,ML,NE,SN,TG"), + "XPF" ->("Fr", 953, 0, "Centime", "CFP Franc", "PF"), + "YER" ->("", 886, 0, "Fils", "Yemeni Rial", "YE"), + "ZAR" ->("R", 710, 2, "Cent", "South African Rand", "ZA"), + "ZMK" ->("ZK", 894, 0, "Ngwee", "Kwacha", "ZM"), + "ZWR" ->("$", 932, 2, "Cent", "Zimbabwe Dollar", "ZW") + ) + + /* Verfies that the given currency code is in the currency list. + */ + private def checkCurrencyCode(currencyCode: String): String = + if (currencyCode.isEmpty) + "" + else if (currencies.contains(currencyCode)) + currencyCode + else + throw new UnknownCurrencyException + + /** Returns the currency symbol for the specified currency. + * For example, returns $ for USD, kr for Swedish Krona, Rs for Indian + * Rupees, etc. + * + * @param currencyCode ISO 4217 three-letter currency code. + * @return currency symbol as string or empty string. + * @throws UnknownCurrencyException if <code>currencyCode</code> + * is not valid. + */ + def getSymbolFor(currencyCode: String): String = + if (currencies.contains(currencyCode)) + currencies(currencyCode)._1 + else if (currencyCode.isEmpty) + "" + else + throw new UnknownCurrencyException + + /** Returns the ISO 4217 numeric code for the specified currency. + * + * @param currencyCode ISO 4217 three-letter currency code. + * @return ISO 4217 numeric currency code. + * @throws UnknownCurrencyException if <code>currencyCode</code> + * is not valid. + */ + def getNumericCodeFor(currencyCode: String): Int = + if (currencies.contains(currencyCode)) + currencies(currencyCode)._2 + else + throw new UnknownCurrencyException + + /** Returns the default number of fraction digits used with the specified currency. + * + * @param currencyCode ISO 4217 three-letter currency code. + * @return number of decimal places for this currency. + * @throws UnknownCurrencyException if <code>currencyCode</code> + * is not valid. + */ + def getDecimalsFor(currencyCode: String): Int = + if (currencies.contains(currencyCode)) + currencies(currencyCode)._3 + else if (currencyCode.isEmpty) + 2 + else + throw new UnknownCurrencyException + + /** Returns the English currency name for the fractional unit of the specified + * currency, e.g. <i>Cent</i> for <i>Euro</i>, <i>Penny</i> for <i>Pound Sterling</i>, + * etc. Always returns empty <code>String</code> for unknown currencies. + * + * @param currencyCode ISO 4217 three-letter currency code. + * @return name of the currency. + * @throws UnknownCurrencyException if <code>currencyCode</code> + * is not valid. + */ + def getFractNameFor(currencyCode: String): String = + if (currencies.contains(currencyCode)) + currencies(currencyCode)._4 + else if (currencyCode.isEmpty) + "" + else + throw new UnknownCurrencyException + + /** Returns the English currency name for the specified currency. + * + * @param currencyCode ISO 4217 three-letter currency code. + * @return name of the currency. + * @throws UnknownCurrencyException if <code>currencyCode</code> + * is not valid. + */ + def getNameFor(currencyCode: String): String = + if (currencies.contains(currencyCode)) + currencies(currencyCode)._5 + else if (currencyCode.isEmpty) + "" + else + throw new UnknownCurrencyException + + /** Returns one or more two-letter ISO 3166-1 country code(s) that indicate where + * the specified currency is in use. + * + * @param currencyCode ISO 4217 three-letter currency code. + * @return Array of String with one or more country code(s). + * @throws UnknownCurrencyException if <code>currencyCode</code> + * is not valid. + */ + def getCountriesFor(currencyCode: String): Array[String] = + if (currencies.contains(currencyCode)) + currencies(currencyCode)._6.split(",") + else + throw new UnknownCurrencyException + + /** Returns a <code>Set</code> containing all 3-letter ISO currency codes as + * Strings. + * + * @return all currency codes. + */ + def getAllCurrencies = currencies.keySet + + /* ----- FACTORY METHODS (String) ----- */ + + /** Constructs a <code>Currency</code> value from a numeric <code>String</code> value. + * + * @param amount specified amount as <code>String</code> value. + * @param currencyCode ISO 4217 three-letter currency code. + * @param decimals number of decimal places. + * @param roundingMode rounding mode to apply. + * @throws UnknownCurrencyException if currency code is not recognised. + */ + def apply(amount: String, currencyCode: String, decimals: Int, roundingMode: RoundingMode): Currency = + new Currency(new BigDec(amount), checkCurrencyCode(currencyCode), decimals, roundingMode) + + /** Constructs a <code>Currency</code> value from a numeric <code>String</code> value + * with default rounding mode (<code>ROUND_HALF_UP</code>). + * + * @param amount specified amount as <code>String</code> value. + * @param currencyCode ISO 4217 three-letter currency code. + * @param decimals number of decimal places. + * @throws UnknownCurrencyException if currency code is not recognised. + */ + def apply(amount: String, currencyCode: String, decimals: Int): Currency = + new Currency(new BigDec(amount), checkCurrencyCode(currencyCode), decimals, ROUND_HALF_UP) + + /** Constructs a <code>Currency</code> value from a numeric <code>String</code> value + * with default rounding mode (<code>ROUND_HALF_UP</code>) and default number of + * decimal places. The number of decimal places is determined by the designated + * currency (e.g. 2 for most currencies, 1 for Chinese Yuan, 0 for Colombian Peso, etc.) + * + * @param amount specified amount as <code>String</code> value. + * @param currencyCode ISO 4217 three-letter currency code. + * @throws UnknownCurrencyException if currency code is not recognised. + */ + def apply(amount: String, currencyCode: String): Currency = + new Currency(new BigDec(amount), checkCurrencyCode(currencyCode), getDecimalsFor(currencyCode), ROUND_HALF_UP) + + /** Constructs a non-specific <code>Currency</code> value from a numeric <code>String</code> value. + * + * @param amount specified amount as <code>String</code> value. + */ + def apply(amount: String): Currency = + new Currency(new BigDec(amount), "", getDecimalsFor(""), ROUND_HALF_UP) + + /** Constructs a non-specific <code>Currency</code> value from a numeric <code>String</code> value + * with the given number of decimal places. + * + * @param amount specified amount as <code>String</code> value. + * @param decimals number of decimal places. + */ + def apply(amount: String, decimals: Int): Currency = + new Currency(new BigDec(amount), "", decimals, ROUND_HALF_UP) + + /** Constructs a non-specific <code>Currency</code> value from a numeric <code>String</code> value + * with the given rounding mode. + * + * @param amount specified amount as <code>String</code> value. + * @param roundingMode rounding mode to apply. + */ + def apply(amount: String, roundingMode: RoundingMode): Currency = + new Currency(new BigDec(amount), "", getDecimalsFor(""), roundingMode) + + /** Constructs a non-specific <code>Currency</code> value from a numeric <code>String</code> value + * with the given number of decimal places and the given rounding mode. + * + * @param amount specified amount as <code>String</code> value. + * @param decimals number of decimal places. + * @param roundingMode rounding mode to apply. + */ + def apply(amount: String, decimals: Int, roundingMode: RoundingMode): Currency = + new Currency(new BigDec(amount), "", decimals, roundingMode) + + /* ----- FACTORY METHODS (Double) ----- */ + + /** Constructs a <code>Currency</code> value from a <code>Double</code> value. + * + * @param amount specified amount as <code>Double</code> value. + * @param currencyCode ISO 4217 three-letter currency code. + * @param decimals number of decimal places. + * @param roundingMode rounding mode to apply. + * @throws UnknownCurrencyException if currency code is not recognised. + */ + def apply(amount: Double, currencyCode: String, decimals: Int, roundingMode: RoundingMode): Currency = + new Currency(BigDec.valueOf(amount), checkCurrencyCode(currencyCode), decimals, roundingMode) + + /** Constructs a <code>Currency</code> value from a <code>Double</code> value + * with default rounding mode (<code>ROUND_HALF_UP</code>). + * + * @param amount specified amount as <code>Double</code> value. + * @param currencyCode ISO 4217 three-letter currency code. + * @param decimals number of decimal places. + * @throws UnknownCurrencyException if currency code is not recognised. + */ + def apply(amount: Double, currencyCode: String, decimals: Int): Currency = + new Currency(BigDec.valueOf(amount), checkCurrencyCode(currencyCode), decimals, ROUND_HALF_UP) + + /** Constructs a <code>Currency</code> value from a <code>Double</code> value + * with default rounding mode (<code>ROUND_HALF_UP</code>) and default number + * of decimal places. The number of decimal places is determined by the designated + * currency (e.g. 2 for most currencies, 1 for Chinese Yuan, 0 for Colombian Peso, etc.) + * + * @param amount specified amount as <code>Double</code> value. + * @param currencyCode ISO 4217 three-letter currency code. + * @throws UnknownCurrencyException if currency code is not recognised. + */ + def apply(amount: Double, currencyCode: String): Currency = + new Currency(BigDec.valueOf(amount), checkCurrencyCode(currencyCode), getDecimalsFor(currencyCode), ROUND_HALF_UP) + + /** Constructs a non-specific <code>Currency</code> value from a <code>Double</code> value. + * + * @param amount specified amount as <code>Double</code> value. + */ + def apply(amount: Double): Currency = + new Currency(BigDec.valueOf(amount), "", getDecimalsFor(""), ROUND_HALF_UP) + + /** Constructs a non-specific <code>Currency</code> value from a <code>Double</code> value + * with the given number of decimal places. + * + * @param amount specified amount as <code>Double</code> value. + * @param decimals number of decimal places. + */ + def apply(amount: Double, decimals: Int): Currency = + new Currency(BigDec.valueOf(amount), "", decimals, ROUND_HALF_UP) + + /** Constructs a non-specific <code>Currency</code> value from a <code>Double</code> value + * with the given rounding mode. + * + * @param amount specified amount as <code>Double</code> value. + * @param roundingMode rounding mode to apply. + */ + def apply(amount: Double, roundingMode: RoundingMode): Currency = + new Currency(BigDec.valueOf(amount), "", getDecimalsFor(""), roundingMode) + + /** Constructs a non-specific <code>Currency</code> value from a <code>Double</code> value + * with the given number of decimal places and the given rounding mode. + * + * @param amount specified amount as <code>Double</code> value. + * @param decimals number of decimal places. + * @param roundingMode rounding mode to apply. + */ + def apply(amount: Double, decimals: Int, roundingMode: RoundingMode): Currency = + new Currency(BigDec.valueOf(amount), "", decimals, roundingMode) + + /* ----- FACTORY METHODS (Long) ----- */ + + /** Constructs a <code>Currency</code> value from a <code>Long</code> value. + * + * @param amount specified amount as <code>Long</code> value. + * @param currencyCode ISO 4217 three-letter currency code. + * @param decimals number of decimal places. + * @param roundingMode rounding mode to apply. + * @throws UnknownCurrencyException if currency code is not recognised. + */ + def apply(amount: Long, currencyCode: String, decimals: Int, roundingMode: RoundingMode): Currency = + new Currency(BigDec.valueOf(amount), checkCurrencyCode(currencyCode), decimals, roundingMode) + + /** Constructs a <code>Currency</code> value from a <code>Long</code> value + * with default rounding mode (<code>ROUND_HALF_UP</code>). + * + * @param amount specified amount as <code>Long</code> value. + * @param currencyCode ISO 4217 three-letter currency code. + * @param decimals number of decimal places. + * @throws UnknownCurrencyException if currency code is not recognised. + */ + def apply(amount: Long, currencyCode: String, decimals: Int): Currency = + new Currency(BigDec.valueOf(amount), checkCurrencyCode(currencyCode), decimals, ROUND_HALF_UP) + + /** Constructs a <code>Currency</code> value from a <code>Long</code> value + * with default rounding mode (<code>ROUND_HALF_UP</code>) and default number of + * decimal places. The number of decimal places is determined by the designated + * currency (e.g. 2 for most currencies, 1 for Chinese Yuan, 0 for Colombian Peso, etc.) + * + * @param amount specified amount as <code>Long</code> value. + * @param currencyCode ISO 4217 three-letter currency code. + * @throws UnknownCurrencyException if currency code is not recognised. + */ + def apply(amount: Long, currencyCode: String): Currency = + new Currency(BigDec.valueOf(amount), checkCurrencyCode(currencyCode), getDecimalsFor(currencyCode), ROUND_HALF_UP) + + /** Constructs a non-specific <code>Currency</code> value from a <code>Long</code> value. + * + * @param amount specified amount as <code>Long</code> value. + */ + def apply(amount: Long): Currency = + new Currency(BigDec.valueOf(amount), "", getDecimalsFor(""), ROUND_HALF_UP) + + /** Constructs a non-specific <code>Currency</code> value from a <code>Long</code> value + * with the given number of decimal places. + * + * @param amount specified amount as <code>Long</code> value. + * @param decimals number of decimal places. + */ + def apply(amount: Long, decimals: Int): Currency = + new Currency(BigDec.valueOf(amount), "", decimals, ROUND_HALF_UP) + + /** Constructs a non-specific <code>Currency</code> value from a <code>Long</code> value + * with the given rounding mode. + * + * @param amount specified amount as <code>Double</code> value. + * @param roundingMode rounding mode to apply. + */ + def apply(amount: Long, roundingMode: RoundingMode): Currency = + new Currency(BigDec.valueOf(amount), "", getDecimalsFor(""), roundingMode) + + /** Constructs a non-specific <code>Currency</code> value from a <code>Long</code> value + * with the given number of decimal places and the given rounding mode. + * + * @param amount specified amount as <code>Long</code> value. + * @param decimals number of decimal places. + * @param roundingMode rounding mode to apply. + */ + def apply(amount: Long, decimals: Int, roundingMode: RoundingMode): Currency = + new Currency(BigDec.valueOf(amount), "", decimals, roundingMode) + +} diff --git a/src/main/scala/task/Decreasable.scala b/src/main/scala/task/Decreasable.scala new file mode 100644 index 0000000..e53df48 --- /dev/null +++ b/src/main/scala/task/Decreasable.scala @@ -0,0 +1,5 @@ +package task + +trait Decreasable extends Accountable { + def decrease(amount: Currency) +} \ No newline at end of file diff --git a/src/main/scala/task/Increasable.scala b/src/main/scala/task/Increasable.scala new file mode 100644 index 0000000..ea180e9 --- /dev/null +++ b/src/main/scala/task/Increasable.scala @@ -0,0 +1,5 @@ +package task + +trait Increasable extends Accountable { + def increase(amount: Currency) +} \ No newline at end of file diff --git a/src/main/scala/task/Result.scala b/src/main/scala/task/Result.scala deleted file mode 100644 index 1d9ef25..0000000 --- a/src/main/scala/task/Result.scala +++ /dev/null @@ -1,12 +0,0 @@ -package task - -case class Target(name: String) - -case class Actor(name: String) - -case class Result( - name: String, - target: Target, - sensorValue: Int, - actor: Actor - ) diff --git a/src/main/scala/task/Robot.scala b/src/main/scala/task/Robot.scala deleted file mode 100644 index 6dfc3c7..0000000 --- a/src/main/scala/task/Robot.scala +++ /dev/null @@ -1,3 +0,0 @@ -package task - -case class Robot(name: String) diff --git a/src/test/scala/BankTests.scala b/src/test/scala/BankTests.scala new file mode 100644 index 0000000..f9ff584 --- /dev/null +++ b/src/test/scala/BankTests.scala @@ -0,0 +1,94 @@ +import org.scalatest.FeatureSpec +import org.scalatest.GivenWhenThen +import org.scalatest.Matchers +import solution.{Transaction, Bank, Person, Account} +import task.{Accountable, Currency} + +class BankTests extends FeatureSpec with GivenWhenThen with Matchers { + info("Test spec for MOST SCROLL task 2.") + + feature("Your implementation of Account") { + scenario("Initialization") { + Given("an new Account") + val a = new Account() + Then("its initial balance should be 0.") + a.balance shouldBe Currency(0, "USD") + } + + scenario("Changing the balance") { + Given("an new Account") + val a = new Account() + Then("changing its balance should work.") + a.increase(Currency(10, "USD")) + a.balance shouldBe Currency(10, "USD") + a.decrease(Currency(10, "USD")) + a.balance shouldBe Currency(0, "USD") + } + } + + feature("Your implementation of Person") { + scenario("Initialization") { + Given("an new Person") + val p = Person("Pete") + Then("its name should be set correctly.") + p.name shouldBe "Pete" + } + } + + feature("Your implementation of Bank") { + scenario("Transferring money") { + Given("some Persons") + val stan = Person("Stan") + val brian = Person("Brian") + + And("some Accounts for them") + val accForStan = new Account(Currency(10.0, "USD")) + val accForBrian = new Account(Currency(0, "USD")) + + And("an new Bank") + val b = new Bank { + stan play new Customer + brian play new Customer + accForStan play new CheckingsAccount + accForBrian play new SavingsAccount + + When("registering accounts to Customers") + +stan addAccount accForStan + +brian addAccount accForBrian + Then("they should be added correctly") + val acc1: List[Accountable] = (+stan).accounts + val acc2: List[Accountable] = (+brian).accounts + acc1 shouldBe List(accForStan) + acc2 shouldBe List(accForBrian) + + And("their balances should be initialized correctly") + accForStan.balance shouldBe Currency(10, "USD") + accForBrian.balance shouldBe Currency(0, "USD") + + When("defining a new transaction") + val transaction = new Transaction(Currency(10.0, "USD")) + accForStan play new transaction.Source + accForBrian play new transaction.Target + + // Defining a partOf relation between Transaction and Bank. + // The transaction needs full access to registered/bound Accounts like + // CheckingsAccount and SavingsAccount. + transaction partOf this + + And("executing it") + transaction play new TransactionRole execute() + + Then("the corresponding balances should be correct") + accForStan.balance shouldBe Currency(0, "USD") + accForBrian.balance shouldBe Currency(9, "USD") + + val c1: List[Currency] = +stan listBalances() + val c2: List[Currency] = +brian listBalances() + + And("listed correctly as well.") + c1 shouldBe List(Currency(0, "USD")) + c2 shouldBe List(Currency(9, "USD")) + } + } + } +} diff --git a/src/test/scala/RobotTests.scala b/src/test/scala/RobotTests.scala deleted file mode 100644 index c1b4f8e..0000000 --- a/src/test/scala/RobotTests.scala +++ /dev/null @@ -1,46 +0,0 @@ -import org.scalatest.FeatureSpec -import org.scalatest.GivenWhenThen -import org.scalatest.Matchers -import scroll.internal.Compartment -import solution.Actor.DrivableRole -import solution.Behavior -import solution.Behavior.ServiceRole -import solution.Navigation.NavigationRole -import solution.Sensor.ObservingRole -import task.Robot - -class RobotTests extends FeatureSpec with GivenWhenThen with Matchers { - info("Test spec for MOST SCROLL task 1.") - - Given("Your solution for the Roles in the Compartments") - val r1 = ServiceRole() - val r2 = NavigationRole() - val r3 = ObservingRole() - val r4 = DrivableRole() - - feature("Compartments and Roles") { - scenario("") { - Then("Calling their behavior should return the expected values.") - r2.getTarget shouldBe task.Target("kitchen") - r3.readSensor shouldBe 100 - r4.getActor shouldBe task.Actor("wheels") - } - - scenario("Merging all together") { - val name = "Pete" - Then("Calling move() should return the expected value.") - val c = new Compartment { - val myRobot = Robot(name) play r1 play r2 play r3 play r4 - - Behavior partOf this - - val r: task.Result = myRobot move() - r shouldBe task.Result( - name, - task.Target("kitchen"), - 100, - task.Actor("wheels")) - } - } - } -} -- GitLab