/**
* Copyright 2010 Archfirst
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.archfirst.common.money;
import static java.math.BigDecimal.ZERO;
import java.io.Serializable;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.NumberFormat;
import java.util.Collection;
import java.util.Currency;
import javax.persistence.Embeddable;
import javax.persistence.Transient;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import org.archfirst.common.quantity.DecimalQuantity;
import org.archfirst.common.quantity.Percentage;
/**
* <P>
* Based on <tt>Money</tt> class published at
* <a href="http://www.javapractices.com/topic/TopicAction.do?Id=13">
* www.javapractices.com
* </a>.
* Modifications include the following:
* <ul>
* <li>JPA annotations have been added to make the class persistent.</li>
* <li>Private setters are provided for JPA to work correctly
* (the class should still be treated as immutable).</li>
* <li>Operations with {@link DecimalQuantity} have been introduced.</li>
* <li><tt>validateState()</tt> method has been modified to remove the
* the scale restriction of the currency. This allows arbitrary
* precision for Money values. This is needed for some applications,
* for example trading, where say a US stock price must be specified
* with 4 decimal precision.</li>
* <li>Scaling operations in <tt>times()</tt> methods have been commented
* out, again to get arbitrary precision amounts. A method called
* <tt>scaleToCurrency()</tt> has been introduced if scaling to currency
* is desired.</li>
* <li><tt>roundToInterval()</tt> method has been added to support financial
* applications.</li>
* </ul>
*
* <h2>Original Documentation</h2>
* <P>Represent an amount of money in any currency.
*
* <P>This class assumes <em>decimal currency</em>, without funky divisions
* like 1/5 and so on. <tt>Money</tt> objects are immutable. Like {@link BigDecimal},
* many operations return new <tt>Money</tt> objects. In addition, most operations
* involving more than one <tt>Money</tt> object will throw a
* <tt>MismatchedCurrencyException</tt> if the currencies don't match.
*
* <h2>Decimal Places and Scale</h2>
* Monetary amounts can be stored in the database in various ways. Let's take
* the example of dollars. It may appear in the database in the following ways :
* <ul>
* <li>as <tt>123456.78</tt>, with the usual number of decimal places
* associated with that currency.
* <li>as <tt>123456</tt>, without any decimal places at all.
* <li>as <tt>123</tt>, in units of thousands of dollars.
* <li>in some other unit, such as millions or billions of dollars.
* </ul>
*
* <P>
* The number of decimal places or style of units is referred to as the
* <em>scale</em> by {@link java.math.BigDecimal}. This class's constructors
* take a <tt>BigDecimal</tt>, so you need to understand it use of the idea
* of scale.
*
* <P>
* The scale can be negative. Using the above examples:
* <table border='1' cellspacing='0' cellpadding='3'>
* <tr>
* <th>Number</th>
* <th>Scale</th>
* </tr>
* <tr>
* <td>123456.78</td>
* <td>2</td>
* </tr>
* <tr>
* <td>123456</td>
* <td>0</td>
* </tr>
* <tr>
* <td>123 (thousands)</td>
* <td>-3</td>
* </tr>
* </table>
*
* <P>
* Note that scale and rounding are two separate issues. In addition, rounding
* is only necessary for multiplication and division operations. It doesn't
* apply to addition and subtraction.
*
* <h2>Operations and Scale</h2>
* <P>
* Operations can be performed on items having <em>different scale</em>. For
* example, these operations are valid (using an <em>ad hoc</em> symbolic
* notation):
*
* <PRE>
* 10.plus(1.23) => 11.23
* 10.minus(1.23) => 8.77
* 10.gt(1.23) => true
* 10.eq(10.00) => true
* </PRE>
*
* This corresponds to typical user expectations. An important exception to this
* rule is that {@link #equals(Object)} is sensitive to scale (while
* {@link #eq(Money)} is not) . That is,
*
* <PRE>
* 10.equals(10.00) => false
* </PRE>
*
* <h2>Multiplication, Division and Extra Decimal Places</h2>
* <P>
* Operations involving multiplication and division are different, since the
* result can have a scale which exceeds that expected for the given currency.
* For example
*
* <PRE>
* ($10.00).times(0.1256) => $1.256
* </PRE>
*
* which has more than two decimals. In such cases,
* <em>this class will always round
* to the expected number of decimal places for that currency.</em>
* This is the simplest policy, and likely conforms to the expectations of most
* end users.
*
* <P>
* This class takes either an <tt>int</tt> or a {@link BigDecimal} for its
* multiplication and division methods. It doesn't take <tt>float</tt> or
* <tt>double</tt> for those methods, since those types don't interact well
* with <tt>BigDecimal</tt>. Instead, the <tt>BigDecimal</tt> class must be
* used when the factor or divisor is a non-integer.
*
* <P>
* <em>The {@link #init(Currency, RoundingMode)} method must be called at least
* once before using the other members of this class.</em>
* It establishes your desired defaults. Typically, it will be called once (and
* only once) upon startup.
*
* <P>
* Various methods in this class have unusually terse names, such as {@link #lt}
* and {@link #gt}. The intent is that such names will improve the legibility
* of mathematical expressions. Example :
*
* <PRE>
* if (amount.lt(hundred)) {
* cost = amount.times(price);
* }
* </PRE>
*/
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "Money")
@Embeddable
public final class Money implements Comparable<Money>, Serializable {
private static final long serialVersionUID = 1L;
/** java.util.Currency does not specify a length, so we do it here */
public static final int CURRENCY_LENGTH = 3;
/** The default currency */
private static Currency defaultCurrency = Currency.getInstance("USD");
/** The rounding mode used for calculations */
private static RoundingMode roundingMode = RoundingMode.HALF_UP;
/** The money amount. Never null. */
@XmlElement(name = "Amount", required = true)
private BigDecimal amount;
/** The currency of the money, such as US Dollars or Euros. Never null. */
@XmlElement(name = "Currency", required = true)
@XmlJavaTypeAdapter(CurrencyAdapter.class)
private Currency currency;
/**
* Set default values for currency and rounding style.
*
* <em>Your application must call this method upon startup</em>. This
* method should usually be called only once (upon startup).
*
* <P>
* The recommended rounding style is {@link RoundingMode#HALF_EVEN}, also
* called <em>banker's rounding</em>; this rounding style introduces the
* least bias.
*
* <P>
* Setting these defaults allow you to use the more terse constructors of
* this class, which are much more convenient.
*
* <P>
* (In a servlet environment, each app has its own classloader. Calling this
* method in one app will never affect the operation of a second app running
* in the same servlet container. They are independent.)
*/
public static void init(
Currency defaultCurrency, RoundingMode roundingMode) {
Money.defaultCurrency = defaultCurrency;
Money.roundingMode = roundingMode;
}
// ----- Constructors -----
public Money() {
this.amount = BigDecimal.ZERO;
this.currency = defaultCurrency;
}
/**
* Full constructor.
*
* @param aAmount is required, can be positive or negative. The number of
* decimals in the amount cannot <em>exceed</em> the maximum
* number of decimals for the given {@link Currency}. It's
* possible to create a <tt>Money</tt> object in terms of
* 'thousands of dollars', for instance. Such an amount would
* have a scale of -3.
* @param aCurrency is required.
*/
public Money(BigDecimal amount, Currency currency) {
this.amount = amount;
this.currency = currency;
validateState();
}
/**
* Constructor taking a String amount
* @param amount
* @param currency
*/
public Money(String amount, Currency currency) {
this(new BigDecimal(amount), currency);
}
/**
* Constructor taking only the money amount.
*
* <P>
* The currency and rounding style both take default values.
*
* @param aAmount is required, can be positive or negative.
*/
public Money(BigDecimal amount) {
this(amount, defaultCurrency);
}
/**
* Constructor taking only a double money amount.
*
* <P>
* The currency and rounding style both take default values.
*
* @param aAmount is required, can be positive or negative.
*/
public Money(Double amount) {
this(new BigDecimal(Double.toString(amount)), defaultCurrency);
}
/**
* Constructor taking a String amount
* @param amount
*/
public Money(String amount) {
this(new BigDecimal(amount), defaultCurrency);
}
// ----- Commands -----
/**
* Add <tt>aThat</tt> <tt>Money</tt> to this <tt>Money</tt>.
* Currencies must match.
*/
public Money plus(Money aThat) {
checkCurrenciesMatch(aThat);
return new Money(amount.add(aThat.amount), currency);
}
/**
* Subtract <tt>aThat</tt> <tt>Money</tt> from this <tt>Money</tt>.
* Currencies must match.
*/
public Money minus(Money aThat) {
checkCurrenciesMatch(aThat);
return new Money(amount.subtract(aThat.amount), currency);
}
/**
* Sum a collection of <tt>Money</tt> objects. Currencies must match. You
* are encouraged to use database summary functions whenever possible,
* instead of this method.
*
* @param moneys collection of <tt>Money</tt> objects, all of the same
* currency. If the collection is empty, then a zero value is
* returned.
* @param currencyIfEmpty is used only when <tt>aMoneys</tt> is empty;
* that way, this method can return a zero amount in the desired
* currency.
*/
public static Money sum(
Collection<Money> moneys, Currency currencyIfEmpty) {
Money sum = new Money(ZERO, currencyIfEmpty);
for (Money money : moneys) {
sum = sum.plus(money);
}
return sum;
}
/**
* Multiply this <tt>Money</tt> by an integral factor.
*
* The scale of the returned <tt>Money</tt> is equal to the scale of
* 'this' <tt>Money</tt>.
*/
public Money times(int aFactor) {
BigDecimal factor = new BigDecimal(aFactor);
BigDecimal newAmount = amount.multiply(factor);
return new Money(newAmount, currency);
}
/**
* Multiply this <tt>Money</tt> by an non-integral factor (having a
* decimal point).
*
* <P>
* The scale of the returned <tt>Money</tt> is equal to the scale of
* 'this' <tt>Money</tt> (this operation is disabled).
*/
public Money times(double aFactor) {
BigDecimal newAmount = amount.multiply(asBigDecimal(aFactor));
// newAmount = newAmount.setScale(getNumDecimalsForCurrency());
return new Money(newAmount, currency);
}
/**
* Multiply this <tt>Money</tt> by an non-integral factor (having a
* decimal point).
*
* <P>
* The scale of the returned <tt>Money</tt> is equal to the scale of
* 'this' <tt>Money</tt> plus the scale of the factor.
*/
public Money times(BigDecimal aFactor) {
BigDecimal newAmount = amount.multiply(aFactor);
// newAmount = newAmount.setScale(getNumDecimalsForCurrency());
return new Money(newAmount, currency);
}
/**
* Multiply this <tt>Money</tt> by a DecimalQuantity.
*
* <P>
* The scale of the returned <tt>Money</tt> is equal to the scale of
* 'this' <tt>Money</tt> plus the scale of the decimal quantity.
*/
public Money times(DecimalQuantity aQuantity) {
return times(aQuantity.getValue());
}
/**
* Divide this <tt>Money</tt> by an integral divisor.
*
* <P>
* The scale of the returned <tt>Money</tt> is as specified.
*/
public Money div(int aDivisor, int scale) {
return div(new BigDecimal(aDivisor), scale);
}
/**
* Divide this <tt>Money</tt> by an non-integral divisor.
*
* <P>
* The scale of the returned <tt>Money</tt> is as specified.
*/
public Money div(double aDivisor, int scale) {
return div(asBigDecimal(aDivisor), scale);
}
/**
* Divide this <tt>Money</tt> by an non-integral divisor.
*
* <P>
* The scale of the returned <tt>Money</tt> is as specified.
*/
public Money div(BigDecimal aDivisor, int scale) {
BigDecimal newAmount = amount.divide(aDivisor, scale, roundingMode);
return new Money(newAmount, currency);
}
/**
* Divide this <tt>Money</tt> by a DecimalQuantity.
*
* <P>
* The scale of the returned <tt>Money</tt> is as specified.
*/
public Money div(DecimalQuantity aDivisor, int scale) {
return div(aDivisor.getValue(), scale);
}
/** Return the absolute value of the amount. */
public Money abs() {
return isPlus() ? this : times(-1);
}
/** Return the amount x (-1). */
public Money negate() {
return times(-1);
}
/**
* Scales this money object to the expected number of decimal places for
* this money's currency. For example,
* <code>
* $100 -> $100.00
* $100.123 -> $100.12
* </code>
*
* @return Money value scaled to currency
*/
public Money scaleToCurrency() {
return setScale(getNumDecimalsForCurrency());
}
public Money setScale(int newScale) {
BigDecimal newAmount = amount.setScale(newScale, roundingMode);
return new Money(newAmount, currency);
}
/**
* Sets the maximum scale of this money object to the specified value.
* The minimum scale is maintained as the expected number of decimal
* places for this money's currency. For example, assuming maxScale = 4,
* following conversions will be performed:
* <code>
* $100 -> $100.00
* $100.1 -> $100.10
* $100.10 -> $100.10
* $100.100 -> $100.10
* $100.101 -> $100.101
* $100.1001 -> $100.1001
* $100.10001 -> $100.0000
* </code>
*
* @param maxScale
* @return
*/
public Money setMaxScale(int maxScale) {
BigDecimal newAmount = amount.stripTrailingZeros();
if (newAmount.scale() < getNumDecimalsForCurrency()) {
newAmount =
newAmount.setScale(getNumDecimalsForCurrency(), roundingMode);
}
else if (newAmount.scale() > maxScale) {
newAmount = newAmount.setScale(maxScale, roundingMode);
}
return new Money(newAmount, currency);
}
/**
* Rounds this money amount to the specified interval. Useful in financial
* markets where a stock price needs to be rounded to the nearest tick size.
* For example:
* <code>
* Price Tick Size -> Rounded Price
* 100.1 0.25 -> 100.00
* 100.2 0.25 -> 100.25
* 100.1 0.125 -> 100.125
* 100.2 0.125 -> 100.250
* </code>
*
* (Based on <a href="http://stackoverflow.com/questions/641848">this</a>
* discussion at stackoverflow.com.)
*
* @param interval for rounding
* @return Money with rounded amount
*/
public Money roundToInterval(BigDecimal interval) {
BigDecimal newAmount =
amount.divide(interval).setScale(0, RoundingMode.HALF_UP).multiply(interval);
return new Money(newAmount, currency);
}
// ----- Queries -----
/**
* Return <tt>true</tt> only if <tt>aThat</tt> <tt>Money</tt> has the
* same currency as this <tt>Money</tt>.
*/
@Transient
public boolean isSameCurrencyAs(Money aThat) {
boolean result = false;
if (aThat != null) {
result = this.currency.equals(aThat.currency);
}
return result;
}
/** Return <tt>true</tt> only if the amount is positive. */
@Transient
public boolean isPlus() {
return amount.compareTo(ZERO) > 0;
}
/** Return <tt>true</tt> only if the amount is negative. */
@Transient
public boolean isMinus() {
return amount.compareTo(ZERO) < 0;
}
/** Return <tt>true</tt> only if the amount is zero. */
@Transient
public boolean isZero() {
return amount.compareTo(ZERO) == 0;
}
/**
* Equals (insensitive to scale).
*
* <P>
* Return <tt>true</tt> only if the amounts are equal. Currencies must
* match. This method is <em>not</em> synonymous with the <tt>equals</tt>
* method.
*/
public boolean eq(Money aThat) {
checkCurrenciesMatch(aThat);
return compareAmount(aThat) == 0;
}
/**
* Greater than.
*
* <P>
* Return <tt>true</tt> only if 'this' amount is greater than 'that'
* amount. Currencies must match.
*/
public boolean gt(Money aThat) {
checkCurrenciesMatch(aThat);
return compareAmount(aThat) > 0;
}
/**
* Greater than or equal to.
*
* <P>
* Return <tt>true</tt> only if 'this' amount is greater than or equal to
* 'that' amount. Currencies must match.
*/
public boolean gteq(Money aThat) {
checkCurrenciesMatch(aThat);
return compareAmount(aThat) >= 0;
}
/**
* Less than.
*
* <P>
* Return <tt>true</tt> only if 'this' amount is less than 'that' amount.
* Currencies must match.
*/
public boolean lt(Money aThat) {
checkCurrenciesMatch(aThat);
return compareAmount(aThat) < 0;
}
/**
* Less than or equal to.
*
* <P>
* Return <tt>true</tt> only if 'this' amount is less than or equal to
* 'that' amount. Currencies must match.
*/
public boolean lteq(Money aThat) {
checkCurrenciesMatch(aThat);
return compareAmount(aThat) <= 0;
}
/**
* Get percentage of this compared to that. For example,
* $6.getPercentage($12, 2) = 50.00%
* @param aThat
* @param fractionalDigits
* @return
*/
@Transient
public Percentage getPercentage(Money aThat, int fractionalDigits) {
checkCurrenciesMatch(aThat);
return new Percentage(amount, aThat.getAmount(), fractionalDigits);
}
/**
* Returns {@link #getAmount()}.getPlainString() + space +
* {@link #getCurrency()}.getSymbol().
*
* <P>
* The return value uses the runtime's <em>default locale</em>, and will
* not always be suitable for display to an end user.
*/
public String toString() {
// This is the original implementation
// return amount.toPlainString() + " " + currency.getSymbol();
// We will use NumberFormat instead
NumberFormat formatter = NumberFormat.getCurrencyInstance();
formatter.setCurrency(currency);
formatter.setMinimumFractionDigits(amount.scale());
return formatter.format(amount);
}
/**
* Like {@link BigDecimal#equals(java.lang.Object)}, this <tt>equals</tt>
* method is also sensitive to scale.
*
* For example, <tt>10</tt> is <em>not</em> equal to <tt>10.00</tt>
* The {@link #eq(Money)} method, on the other hand, is <em>not</em>
* sensitive to scale.
*/
public boolean equals(Object object) {
if (this == object) {
return true;
}
if (!(object instanceof Money)) {
return false;
}
Money that = (Money)object;
// the object fields are never null :
boolean result = (this.amount.equals(that.amount));
result = result && (this.currency.equals(that.currency));
return result;
}
public int hashCode() {
return this.amount.hashCode() + this.currency.hashCode();
}
public int compareTo(Money that) {
if (this == that) {
return 0;
}
// the object fields are never null
int comparison = this.amount.compareTo(that.amount);
if (comparison != 0) {
return comparison;
}
comparison = this.currency.getCurrencyCode().compareTo(
that.currency.getCurrencyCode());
if (comparison != 0) {
return comparison;
}
return 0;
}
@Transient
private int getNumDecimalsForCurrency() {
return currency.getDefaultFractionDigits();
}
/** Ignores scale: 0 same as 0.00 */
private int compareAmount(Money aThat) {
return this.amount.compareTo(aThat.amount);
}
private BigDecimal asBigDecimal(double aDouble) {
String asString = Double.toString(aDouble);
return new BigDecimal(asString);
}
private void validateState() {
if (amount == null) {
throw new IllegalArgumentException("Amount cannot be null");
}
if (currency == null) {
throw new IllegalArgumentException("Currency cannot be null");
}
// if (amount.scale() > getNumDecimalsForCurrency()) {
// throw new IllegalArgumentException("Number of decimals is "
// + amount.scale() + ", but currency only takes "
// + getNumDecimalsForCurrency() + " decimals.");
// }
}
private void checkCurrenciesMatch(Money aThat) {
if (!this.currency.equals(aThat.getCurrency())) {
throw new MismatchedCurrencyException(aThat.getCurrency()
+ " doesn't match the expected currency : " + currency);
}
}
// ----- Getters and Setters -----
/** Returns the amount */
public BigDecimal getAmount() {
return amount;
}
private void setAmount(BigDecimal amount) {
this.amount = amount;
}
/** Returns the currency */
public Currency getCurrency() {
return currency;
}
private void setCurrency(Currency currency) {
this.currency = currency;
}
// ----- Inner Classes -----
/**
* Thrown when a set of <tt>Money</tt> objects do not have matching
* currencies. For example, adding together Euros and Dollars does not make
* any sense.
*/
public static final class MismatchedCurrencyException
extends RuntimeException {
private static final long serialVersionUID = 1L;
MismatchedCurrencyException(String aMessage) {
super(aMessage);
}
}
}