org.javamoney.moneta.convert.DefaultExchangeRate Maven / Gradle / Ivy
/*
* Copyright (c) 2012, 2015, Credit Suisse (Anatole Tresch), Werner Keil and others by the @author tag.
*
* 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.javamoney.moneta.convert;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import javax.money.CurrencyUnit;
import javax.money.NumberValue;
import javax.money.convert.ConversionContext;
import javax.money.convert.ExchangeRate;
/**
* This class models an exchange rate, which defines the factor the numeric value of a base amount in some currency
* 'A' must be multiplied
* to get the corresponding amount in the terminating currency 'B'. Hereby
*
* - an exchange rate always models one rate from a base (source) to a term
* (target) {@link javax.money.CurrencyUnit}.
* - an exchange rate is always bound to a rate type, which typically matches
* the data source of the conversion data, e.g. different credit card providers
* may use different rates for the same conversion.
* - an exchange rate may restrict its validity. In most of the use cases a
* rates' validity will be well defined, but it is also possible that the data
* provider is not able to support the rate's validity, leaving it undefined-
* - an exchange rate has a provider, which is responsible for defining the
* rate. A provider hereby may be, but must not be the same as the rate's data
* source.
* - an exchange rate can be a direct rate, where its factor is
* represented by a single conversion step. Or it can model a derived
* rate, where multiple conversion steps are required to define the overall
* base/term conversion. In case of derived rates the chained rates define the
* overall factor, by multiplying the individual chain rate factors. Of course,
* this also requires that each subsequent rate's base currency in the chain
* does match the previous term currency (and vice versa):
* - Whereas the factor should be directly implied by the format rate chain
* for derived rates, this is obviously not the case for the validity range,
* since rates can have a undefined validity range. Nevertheless in many cases
* also the validity range can (but must not) be derived from the rate chain.
* - Finally a conversion rate is always unidirectional. There might be cases
* where the reciprocal value of {@link #factor} matches the correct reverse
* rate. But in most use cases the reverse rate either has a different rate (not
* equal to the reciprocal value), or might not be defined at all. Therefore for
* reversing a ExchangeRate one must access an {@link javax.money.convert.ExchangeRateProvider} and
* query for the reverse rate.
*
*
* The class also implements {@link Comparable} to allow sorting of multiple
* exchange rates using the following sorting order;
*
* - Exchange rate type
* - Exchange rate provider
* - base currency
* - term currency
*
*
* Finally ExchangeRate is modeled as an immutable and thread safe type. Also
* exchange rates are {@link java.io.Serializable}, hereby serializing in the following
* form and order:
*
* - The base {@link javax.money.CurrencyUnit}
*
- The target {@link javax.money.CurrencyUnit}
*
- The factor (NumberValue)
*
- The {@link javax.money.convert.ConversionContext}
*
- The rate chain
*
*
* @author Werner Keil
* @author Anatole Tresch
* @see Wikipedia:
* Exchange Rate (Quotations)
*/
class DefaultExchangeRate implements ExchangeRate, Serializable, Comparable {
// TODO this should probably go to "convert" in future releases. Analyze feasability of refactoring.
/**
* serialVersionUID.
*/
private static final long serialVersionUID = 5077295306570465837L;
/**
* The base currency.
*/
private final CurrencyUnit base;
/**
* The terminating currency.
*/
private final CurrencyUnit term;
/**
* The conversion factor.
*/
private final NumberValue factor;
/**
* The {@link javax.money.convert.ConversionContext}
*/
private final ConversionContext conversionContext;
/**
* The full chain, at least one instance long.
*/
private final List chain = new ArrayList<>();
/**
* Creates a new instance with a custom chain of exchange rate type, e.g. or
* creating derived rates.
*
* @param builder The Builder, never {@code null}.
*/
DefaultExchangeRate(ExchangeRateBuilder builder) {
Objects.requireNonNull(builder.base, "base may not be null.");
Objects.requireNonNull(builder.term, "term may not be null.");
Objects.requireNonNull(builder.factor, "factor may not be null.");
Objects.requireNonNull(builder.conversionContext, "exchangeRateType may not be null.");
this.base = builder.base;
this.term = builder.term;
this.factor = builder.factor;
this.conversionContext = builder.conversionContext;
setExchangeRateChain(builder.rateChain);
}
/**
* Internal method to set the rate chain, which also ensure that the chain
* passed, when not null, contains valid elements.
*
* @param chain the chain to set.
*/
private void setExchangeRateChain(List chain) {
this.chain.clear();
if (Objects.isNull(chain) || chain.isEmpty()) {
this.chain.add(this);
} else {
for (ExchangeRate rate : chain) {
if (Objects.isNull(rate)) {
throw new IllegalArgumentException("Rate Chain element can not be null.");
}
}
this.chain.addAll(chain);
}
}
/**
* Access the {@link javax.money.convert.ConversionContext} of {@link javax.money.convert.ExchangeRate}.
*
* @return the conversion context, never null.
*/
@Override
public final ConversionContext getContext() {
return this.conversionContext;
}
/**
* Get the base (source) {@link javax.money.CurrencyUnit}.
*
* @return the base {@link javax.money.CurrencyUnit}.
*/
@Override
public final CurrencyUnit getBaseCurrency() {
return this.base;
}
/**
* Get the term (target) {@link javax.money.CurrencyUnit}.
*
* @return the term {@link javax.money.CurrencyUnit}.
*/
@Override
public final CurrencyUnit getCurrency() {
return this.term;
}
/**
* Access the rate's bid factor.
*
* @return the bid factor for this exchange rate, or {@code null}.
*/
@Override
public final NumberValue getFactor() {
return this.factor;
}
/**
* Access the chain of exchange rates.
*
* @return the chain of rates, in case of a derived rate, this may be
* several instances. For a direct exchange rate, this equals to
* new ExchangeRate[]{this}
.
*/
@Override
public final List getExchangeRateChain() {
return this.chain;
}
/**
* Allows to evaluate if this exchange rate is a derived exchange rate.
*
* Derived exchange rates are defined by an ordered list of subconversions
* with intermediate steps, whereas a direct conversion is possible in one
* steps.
*
* This method always returns {@code true}, if the chain contains more than
* one rate. Direct rates, have also a chain, but with exact one rate.
*
* @return true, if the exchange rate is derived.
*/
@Override
public final boolean isDerived() {
return this.chain.size() > 1;
}
/*
* (non-Javadoc)
*
* @see java.lang.Comparable#compareTo(java.lang.Object)
*/
@Override
public int compareTo(ExchangeRate o) {
Objects.requireNonNull(o);
int compare = this.getBaseCurrency().getCurrencyCode().compareTo(o.getBaseCurrency().getCurrencyCode());
if (compare == 0) {
compare = this.getCurrency().getCurrencyCode().compareTo(o.getCurrency().getCurrencyCode());
}
if (compare == 0) {
compare = this.getContext().getProviderName().compareTo(o.getContext().getProviderName());
}
return compare;
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return "ExchangeRate [base=" + base + ", term=" + term + ", factor=" + factor + ", conversionContext=" + conversionContext + ']';
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
// Numerically equal factors should produce the same hash code, so hash a normalized
// version of the factor rather than the NumberValue object.
BigDecimal normalizedFactor = factor.numberValue(BigDecimal.class).stripTrailingZeros();
// The exchange rate chain includes a reference to "this" if the caller doesn't explicitly
// set a chain in the builder, so we can't naively hash the chain or we'll get infinite
// recursion.
int chainHash = 0;
for (ExchangeRate chainedRate : chain) {
if (chainedRate == this) {
// Use a constant to represent the presence of this object in the chain.
chainHash = Objects.hash(chainHash, "this");
} else {
chainHash = Objects.hash(chainHash, chainedRate);
}
}
return Objects.hash(base, conversionContext, normalizedFactor, term, chainHash);
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if(obj instanceof DefaultExchangeRate) {
DefaultExchangeRate other = (DefaultExchangeRate) obj;
return Objects.equals(base, other.base) &&
Objects.equals(conversionContext, other.conversionContext) &&
factor.compareTo(other.factor) == 0 &&
Objects.equals(term, other.term);
}
return false;
}
/**
* Create a {@link ExchangeRateBuilder} based on the current rate instance.
*
* @return a new {@link ExchangeRateBuilder}, never {@code null}.
*/
public ExchangeRateBuilder toBuilder() {
return new ExchangeRateBuilder(getContext()).setBase(getBaseCurrency()).setTerm(getCurrency())
.setFactor(getFactor()).setRateChain(getExchangeRateChain());
}
}