/*
* Copyright (c) 2005-2011 Grameen Foundation USA
* All rights reserved.
*
* 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.
*
* See also http://www.apache.org/licenses/LICENSE-2.0.html for an
* explanation of the license and how it is applied.
*/
package org.mifos.framework.util.helpers;
import java.io.Serializable;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.Locale;
import org.mifos.application.master.business.MifosCurrency;
import org.mifos.config.AccountingRules;
import org.mifos.core.CurrencyMismatchException;
/**
* This class represents Money objects in the system, it should be used for all
* financial operations like addition,subtraction etc of money. As of now it
* deals with only one currency but later it can be extended to handle currency
* conversions while performing operations. This is an immutable class as the
* money object is not supposed to be modified .
*
*/
public final class Money implements Serializable, Comparable<Money> {
/**
* The precision used for internal calculations
* 7 (before decimal) + 6(after decimal) = 13.
* Assuming that we are bounding the calculations to 7 digits
* before decimal, we get 6 digits after the decimal which is enough
* for the precision.
* <br><br>
* Why we bound the before decimal digits to 7 ?
* <br>see latest_schema.sql for amount (DECIMAL(10,3))
*
*/
private static int internalPrecision = 13;
/**
* The rounding mode used for internal calculations.
*/
private static RoundingMode internalRoundingMode = RoundingMode.HALF_UP;
private static MifosCurrency defaultCurrency;
private final MifosCurrency currency;
private final BigDecimal amount;
/**
* This creates a Money object with currency set to MFICurrency and amount
* set to zero.
*/
public Money(MifosCurrency currency) {
this(currency, new BigDecimal(0));
}
public Money(MifosCurrency currency, Double amount) {
this(currency, new BigDecimal(amount));
}
public Money(MifosCurrency currency, String amount) {
this(currency, new BigDecimal(amount));
}
public Money(MifosCurrency currency, BigDecimal amount) {
checkCurrencyNotNull(currency);
checkAmountNotNull(amount);
this.currency = currency;
this.amount = amount.setScale(internalPrecision, internalRoundingMode);
}
public BigDecimal getAmount() {
return this.amount;
}
/**
* Don't use double with Money as floating point calculation can cause loss in precision
* <br />
* <b>Use</b> {@link #getAmount()}
* @return
*/
@Deprecated
public double getAmountDoubleValue() {
return amount.doubleValue();
}
public MifosCurrency getCurrency() {
return currency;
}
public static MifosCurrency getDefaultCurrency() {
return defaultCurrency;
}
public static void setDefaultCurrency(MifosCurrency defaultCurrency) {
Money.defaultCurrency = defaultCurrency;
}
/**
* If the object passed as parameter is null or if its currency or amount is
* null it returns this else performs the required operation and returns a
* new Money object corresponding to the value.
*/
public Money add(Money money) {
if (money == null) {
return this;
}
checkCurrenciesDifferent(this, money);
return new Money(currency, amount.add(money.getAmount()));
}
/**
* If the object passed as parameter is null or if its currency or amount is
* null it returns this else performs the required operation and returns a
* new Money object corresponding to the value.
*/
public Money subtract(Money money) {
if (money == null) {
return this;
}
checkCurrenciesDifferent(this, money);
return new Money(money.getCurrency(), amount.subtract(money.getAmount()));
}
//for M5193
public Money multiply(Double factor,int days,int duration){
return multiply(new BigDecimal(factor),new BigDecimal(days),new BigDecimal(duration));
}
public Money multiply(BigDecimal factor,BigDecimal days,BigDecimal duration){
BigDecimal total=(factor.multiply(days));
factor=total.divide(duration,internalPrecision, internalRoundingMode);
return new Money(currency,amount.multiply(factor).setScale(internalPrecision, internalRoundingMode));
}//end
public Money multiply(Double factor) {
return multiply(new BigDecimal(factor));
}
public Money multiply(BigDecimal factor) {
return new Money(currency, amount.multiply(factor).setScale(internalPrecision, internalRoundingMode));
}
public Money multiply(int intValue) {
return multiply(new BigDecimal(intValue));
}
/**
* Dividing by Money gives a fractional value <br>
* <br>
* e.g. Money(USD)/Money(USD) = fraction (no unit) <br>
* <br>
*/
public BigDecimal divide(Money money) {
checkCurrenciesDifferent(this, money);
return amount.divide(money.getAmount(), internalPrecision, internalRoundingMode);
}
public Money divide(BigDecimal factor) {
return new Money(currency, amount.divide(factor.setScale(internalPrecision, internalRoundingMode),
internalPrecision, internalRoundingMode));
}
public Money divide(Double value) {
return divide(new BigDecimal(value));
}
public Money divide(Short shortVal) {
return divide(new BigDecimal(shortVal));
}
public Money divide(Integer intVal) {
return divide(new BigDecimal(intVal));
}
public Money negate() {
// no need to set scale since negation preserves scale
return new Money(currency, amount.negate());
}
/**
* This method returns a new Money object with currency same as current
* currency and amount calculated after rounding based on rounding mode and
* roundingAmount where in both are obtained from MifosCurrency object. <br />
* <br />
* The rounding calculation is as follows:- Lets say we want to round 142.34
* to nearest 50 cents and and rounding mode is ceil (i.e. to greater
* number) we will divide 142.34 by .5 which will result in 284.68 now we
* will round this to a whole number using ceil mode which will result in
* 285 and then multiply 285 by 0.5 resulting in 142.5.
*
*/
public static Money round(Money money, BigDecimal roundOffMultiple, RoundingMode roundingMode) {
// insure that we are using the correct internal precision
BigDecimal roundingAmount = roundOffMultiple.setScale(internalPrecision, internalRoundingMode);
// FIXME: are we loosing precision here
// mathcontext only take cares of significant digits
// not digit right to the decimal
BigDecimal nearestFactor = money.getAmount().divide(roundingAmount,
new MathContext(internalPrecision, internalRoundingMode));
nearestFactor = nearestFactor.setScale(0, roundingMode);
BigDecimal roundedAmount = nearestFactor.multiply(roundingAmount);
return new Money(money.getCurrency(), roundedAmount);
}
/**
* This method return true if the currency associated with the two money
* objects is equal and also the compareTo method of BigDecimal return 0 for
* the amount of the two money objects. It is not advisable to use equals
* method of BigDecimal because it would return false for numbers like 10.0
* and 10.00 instead we should use compareTo.
*
*/
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Money)) {
return false;
}
if (obj == this) {
return true;
}
Money money = (Money) obj;
return this.currency.equals(money.getCurrency()) && (this.getAmount().compareTo(money.getAmount()) == 0);
}
@Override
public int hashCode() {
return this.currency.getCurrencyId() * 100 + this.getAmount().intValue();
}
@Override
public String toString() {
return toString(getDigitsAfterDecimal());
}
/**
* @deprecated - remove use of static AccountingRules
*/
@Deprecated
Short getDigitsAfterDecimal() {
return AccountingRules.getDigitsAfterDecimal(getCurrency());
}
public String toString(Short digitsAfterDecimal) {
// FIXME string formating based on Accounting rule should be done in
// MoneyUtil class
// only string representation of BigDecimal should be returned here
double doubleValue = amount.doubleValue();
String format = "%." + digitsAfterDecimal.toString() + "f";
String formatStr = String.format(Locale.ENGLISH, format, 0.0);
NumberFormat numberFormat = NumberFormat.getInstance(Locale.ENGLISH);
DecimalFormat decimalFormat = null;
if (numberFormat instanceof DecimalFormat) {
decimalFormat = ((DecimalFormat) numberFormat);
decimalFormat.applyPattern(formatStr);
return decimalFormat.format(doubleValue);
}
return numberFormat.format(doubleValue);
}
public boolean isGreaterThan(Money money) {
return this.compareTo(money) > 0;
}
public boolean isGreaterThanOrEqual(Money money) {
return this.compareTo(money) >= 0;
}
public boolean isGreaterThanZero() {
return this.getAmount().compareTo(BigDecimal.ZERO) > 0;
}
public boolean isGreaterThanOrEqualZero() {
return this.getAmount().compareTo(BigDecimal.ZERO) >= 0;
}
public boolean isLessThan(Money money) {
return this.compareTo(money) < 0;
}
public boolean isLessThanOrEqual(Money money) {
return this.compareTo(money) <= 0;
}
public boolean isLessThanZero() {
return this.getAmount().compareTo(BigDecimal.ZERO) < 0;
}
public boolean isLessThanOrEqualZero() {
return this.getAmount().compareTo(BigDecimal.ZERO) <= 0;
}
public boolean isZero() {
return this.getAmount().compareTo(BigDecimal.ZERO) == 0;
}
public boolean isNonZero() {
return this.getAmount().compareTo(BigDecimal.ZERO) != 0;
}
private void checkCurrencyNotNull(MifosCurrency currency) {
if (currency == null) {
throw new NullPointerException(ExceptionConstants.CURRENCY_MUST_NOT_BE_NULL);
}
}
private void checkAmountNotNull(BigDecimal amount) {
if (amount == null) {
throw new NullPointerException(ExceptionConstants.AMMOUNT_MUST_NOT_BE_NULL);
}
}
private static void checkCurrenciesDifferent(Money m1, Money m2) {
if (!m1.getCurrency().getCurrencyId().equals(m2.getCurrency().getCurrencyId())) {
throw new CurrencyMismatchException(ExceptionConstants.ILLEGALMONEYOPERATION);
}
}
@Override
public int compareTo(Money money) {
checkCurrenciesDifferent(this, money);
return this.getAmount().compareTo(money.amount);
}
public static Money zero() {
return new Money(Money.getDefaultCurrency(), "0");
}
public static Money zero(MifosCurrency currency) {
return new Money(currency, "0");
}
public boolean isTinyAmount() {
double delta = 9 * Math.pow(10, -(getDigitsAfterDecimal() + Short.valueOf("1")));
return Math.abs(this.getAmount().doubleValue()) <= delta;
}
public static int getInternalPrecision() {
return internalPrecision;
}
}