/*
* Copyright (C) 2011 4th Line GmbH, Switzerland
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.fourthline.konto.shared.entity;
import org.fourthline.konto.shared.Constants;
import org.seamless.gwt.validation.shared.EntityProperty;
import org.fourthline.konto.shared.MonetaryAmount;
import org.seamless.gwt.validation.shared.Validatable;
import org.seamless.gwt.validation.shared.ValidationError;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import javax.persistence.Transient;
import javax.persistence.UniqueConstraint;
import java.io.Serializable;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
/**
* http://ec.europa.eu/economy_finance/euro/adoption/conversion/index_en.htm
* <p/>
* Conversion rates are used for converting euro to national currency units and vice versa.
* Inverse rates derived from the conversion rates may not be used. Converting a national
* currency unit into the euro unit consists in dividing the amount expressed in the national
* currency unit by the conversion rate of the euro against this national currency unit.
* <p/>
* Examples:
* 12 euro = 12 * 6.55957 = 78.7148 francs = 78.71 francs
* 52 francs = 52 / 6.55957 = 7.9273 euro = 7.93 euro
* <p/>
* Monetary amounts to be converted from one national currency unit into another shall first
* be converted into a monetary amount expressed in the euro unit, whose amount may be rounded
* to not less than three decimals and shall then be converted into the other national currency
* unit. No alternative method (in particular, that which consists in calculating directly
* the bilateral conversion rates between national currency units using the euro conversion
* rates of the latter) may be used unless it produces the same results.
* <p/>
* Example:
* 1 euro = 1.95583 mark
* 54 francs = 54 / 6.55957 = 8.23224 euro
* 8.232 euro = 8.232 * 1.95583 = 16.1003 marks = 16.10 marks
* <p/>
* http://www.banque-france.fr/gb/eurosys/telechar/docs/arrondis.pdf
* <br/>
* http://www.banana.ch/accounting/files/multicurrency_eng.pdf
*
* @author Christian Bauer
*/
@Entity
@Table(name = "CURRENCY_PAIR",
uniqueConstraints = @UniqueConstraint(columnNames = {"FROM_CODE", "TO_CODE", "CREATED_ON"})
)
public class CurrencyPair implements Serializable, Validatable {
public static final int PRECISION = 10;
public static final int SCALE = 6;
public static final BigDecimal DEFAULT_EXCHANGE_RATE =
new BigDecimal(1).setScale(CurrencyPair.SCALE, RoundingMode.HALF_UP);
public static enum Property implements EntityProperty {
id,
fromCode,
toCode,
exchangeRate,
createdOn
}
@Id
@GeneratedValue(generator = Constants.SEQUENCE_NAME)
@Column(name = "CURRENCY_PAIR_ID")
private Long id;
@Column(name = "FROM_CODE", nullable = false)
private String fromCode;
@Column(name = "TO_CODE", nullable = false)
private String toCode;
@Column(name = "EXCHANGE_RATE",
nullable = false,
precision = CurrencyPair.PRECISION,
scale = CurrencyPair.SCALE
)
private BigDecimal exchangeRate = DEFAULT_EXCHANGE_RATE;
@Temporal(TemporalType.DATE)
@Column(name = "CREATED_ON", nullable = false)
private Date createdOn = new Date();
@Transient
private MonetaryUnit fromUnit;
@Transient
private MonetaryUnit toUnit;
public CurrencyPair() {
}
public CurrencyPair(String fromCode, String toCode) {
this(fromCode, toCode, DEFAULT_EXCHANGE_RATE);
}
public CurrencyPair(MonetaryUnit fromUnit, MonetaryUnit toUnit, MonetaryAmount originalAmount, MonetaryAmount exchangedAmount) {
this(fromUnit.getCurrencyCode(),
toUnit.getCurrencyCode(),
originalAmount != null && exchangedAmount != null
? getExchangeRate(originalAmount, exchangedAmount)
: DEFAULT_EXCHANGE_RATE
);
this.fromUnit = fromUnit;
this.toUnit = toUnit;
}
public CurrencyPair(MonetaryUnit fromUnit, MonetaryUnit toUnit, String exchangeRate) {
this(fromUnit.getCurrencyCode(), toUnit.getCurrencyCode(), new BigDecimal(exchangeRate));
this.fromUnit = fromUnit;
this.toUnit = toUnit;
}
public CurrencyPair(MonetaryUnit fromUnit, MonetaryUnit toUnit) {
this(fromUnit, toUnit, DEFAULT_EXCHANGE_RATE);
}
public CurrencyPair(MonetaryUnit fromUnit, MonetaryUnit toUnit, BigDecimal exchangeRate) {
this(fromUnit.getCurrencyCode(), toUnit.getCurrencyCode(), exchangeRate);
this.fromUnit = fromUnit;
this.toUnit = toUnit;
}
public CurrencyPair(String fromCode, String toCode, BigDecimal exchangeRate) {
this.fromCode = fromCode;
this.toCode = toCode;
setExchangeRate(exchangeRate);
}
public CurrencyPair(String fromCode, String toCode, BigDecimal exchangeRate, Date createdOn) {
this.fromCode = fromCode;
this.toCode = toCode;
setExchangeRate(exchangeRate);
this.createdOn = createdOn;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getFromCode() {
return fromCode;
}
public void setFromCode(String fromCode) {
this.fromCode = fromCode;
}
public String getToCode() {
return toCode;
}
public void setToCode(String toCode) {
this.toCode = toCode;
}
public BigDecimal getExchangeRate() {
return exchangeRate;
}
public String getExchangeRateString() {
// Cut off the remaining zeroes of fraction
StringBuilder sb = new StringBuilder();
sb.append(getExchangeRate().toString());
int dot = sb.lastIndexOf(".");
for (int i = sb.length() -1; i >= dot && (sb.charAt(i) == '0' || sb.charAt(i) == '.'); i--) {
sb.deleteCharAt(i);
}
return sb.toString();
}
public boolean isValidExchangeRate(BigDecimal er) {
// Make sure it fits within our constraints for the database type etc.
if (er.precision() > PRECISION)
return false;
if (er.signum() <= 0)
return false;
return true;
}
/**
* @param er The desired rate.
* @throws IllegalArgumentException if the rate is not valid (negative, zero, invalid precision, etc.)
*/
public void setExchangeRate(BigDecimal er) throws IllegalArgumentException {
if (!isValidExchangeRate(er))
throw new IllegalArgumentException("Invalid exchange rate '" + getFromCode() + "/" + getToCode() + "': " + er);
this.exchangeRate = er.setScale(CurrencyPair.SCALE, RoundingMode.HALF_UP);
}
public Date getCreatedOn() {
return createdOn;
}
public void setCreatedOn(Date createdOn) {
this.createdOn = createdOn;
}
public MonetaryUnit getFromUnit() {
return fromUnit;
}
public void setFromUnit(MonetaryUnit fromUnit) {
this.fromUnit = fromUnit;
}
public MonetaryUnit getToUnit() {
return toUnit;
}
public void setToUnit(MonetaryUnit toUnit) {
this.toUnit = toUnit;
}
static public BigDecimal getExchangeRate(MonetaryAmount fromAmount, MonetaryAmount toAmount) {
if (toAmount.abs().signum() == 0) return DEFAULT_EXCHANGE_RATE; // Avoid divide by zero
return toAmount.getValue().abs().divide(
fromAmount.getValue().abs(), CurrencyPair.SCALE, RoundingMode.HALF_UP
);
}
public MonetaryAmount getExchangedAmount(MonetaryAmount originalAmount) {
return new MonetaryAmount(
toUnit,
originalAmount.getValue().multiply(getExchangeRate())
);
}
public MonetaryAmount getOriginalAmount(MonetaryAmount exchangedAmount) {
// A scale of 32 should be enough, it is cut to unit's scale next anyway
BigDecimal original =
exchangedAmount.getValue()
.divide(getExchangeRate(), 32, RoundingMode.HALF_UP);
return new MonetaryAmount(getFromUnit(), original);
}
public boolean equalsFromToCodes(CurrencyPair that) {
return (getFromCode().equals(that.getFromCode())
&& getToCode().equals(that.getToCode()));
}
public static List<CurrencyPair> getPairs(List<MonetaryUnit> monetaryUnits) {
// Always sort the collection so result is guaranteed to be the same
List<MonetaryUnit> sortedUnits = new ArrayList(monetaryUnits);
Collections.sort(
sortedUnits,
new Comparator<MonetaryUnit>() {
@Override
public int compare(MonetaryUnit a, MonetaryUnit b) {
return a.getCurrencyCode().compareToIgnoreCase(b.getCurrencyCode());
}
});
List<CurrencyPair> pairs = new ArrayList();
for (MonetaryUnit unitA : sortedUnits) {
for (MonetaryUnit unitB : sortedUnits) {
if (unitA.equals(unitB)) continue;
pairs.add(new CurrencyPair(unitA.getCurrencyCode(), unitB.getCurrencyCode()));
}
}
return pairs;
}
@Override
public List<ValidationError> validate(String group) {
return new ArrayList();
}
@Override
public String toString() {
return getFromCode() + "/" + getToCode() + ", " + getExchangeRate() + ", " + getCreatedOn();
}
}