package com.mygeopay.core.util;
/**
* Copyright 2014 Andreas Schildbach
* Copyright 2015 John L. Jegutanis
*
* 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.
*/
import com.mygeopay.core.coins.CoinType;
import com.mygeopay.core.coins.Value;
import com.mygeopay.core.coins.ValueType;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import java.math.BigDecimal;
import java.math.RoundingMode;
import org.bitcoinj.core.Coin;
/**
* An exchange rate is expressed as a ratio of a pair of {@link Value} amounts.
*/
public class ExchangeRateBase implements ExchangeRate {
private static final int RATE_SCALE = 10;
public static final String ZERO_RATE_ERROR_MESSAGE = "Exchange rate cannot be zero";
public final Value value1;
public final Value value2;
/** Construct exchange rate. This amount of coin is worth that amount of fiat. */
public ExchangeRateBase(Value value1, Value value2) {
this.value1 = checkNonZero(value1);
this.value2 = checkNonZero(value2);
}
public ExchangeRateBase(ValueType type1, ValueType type2, String rateString) {
// Make the rate having maximum of RATE_SCALE decimal places
BigDecimal rate = new BigDecimal(rateString)
.setScale(RATE_SCALE, RoundingMode.HALF_UP).stripTrailingZeros();
checkState(rate.signum() >= 0); // rate cannot be negative
// Check if the rate has too many decimal places or scale() for the type2 to handle.
if (rate.scale() > type2.getUnitExponent()) {
// If we have too many decimal places, multiply everything by a factor so that the rate
// can fit in a type2 value. For example if the rate is 0.123456789 but a type2 can only
// handle 4 places, then final result will be value1 = 100000 and value2 = 12345.6789
BigDecimal rateFactor = BigDecimal.TEN.pow(rate.scale() - type2.getUnitExponent());
value1 = type1.oneCoin().multiply(rateFactor.longValue());
value2 = Value.parse(type2, rate.multiply(rateFactor));
} else {
value1 = type1.oneCoin();
value2 = Value.parse(type2, rate);
}
checkNonZero(value1);
checkNonZero(value2);
}
private static Value checkNonZero(Value value) {
checkArgument(!value.isZero(), ZERO_RATE_ERROR_MESSAGE);
return value;
}
@Override
public Value convert(CoinType type, Coin coin) {
return convertValue(type.value(coin));
}
@Override
public Value convert(Value convertingValue) {
return convertValue(convertingValue);
}
@Override
public ValueType getOtherType(ValueType type) {
checkIfValueTypeAvailable(type);
if (value1.type.equals(type)) {
return value2.type;
} else {
return value1.type;
}
}
@Override
public ValueType getSourceType() {
return value1.type;
}
@Override
public ValueType getDestinationType() {
return value2.type;
}
@Override
public boolean canConvert(ValueType type1, ValueType type2) {
try {
checkIfValueTypeAvailable(type1);
checkIfValueTypeAvailable(type2);
return true;
} catch (IllegalArgumentException ignored) {
return false;
}
}
protected Value convertValue(Value convertingValue) {
checkIfValueTypeAvailable(convertingValue.type);
Value rateFrom = getFromRateValue(convertingValue.type);
Value rateTo = getToRateValue(convertingValue.type);
// Use BigDecimal because it's much easier to maintain full precision without overflowing.
final BigDecimal converted = BigDecimal.valueOf(convertingValue.value)
.multiply(BigDecimal.valueOf(rateTo.value))
.divide(BigDecimal.valueOf(rateFrom.value), RoundingMode.HALF_UP);
if (converted.compareTo(BigDecimal.valueOf(Long.MAX_VALUE)) > 0
|| converted.compareTo(BigDecimal.valueOf(Long.MIN_VALUE)) < 0)
throw new ArithmeticException("Overflow");
return Value.valueOf(rateTo.type, converted.longValue());
}
protected Value getFromRateValue(ValueType fromType) {
if (value1.type.equals(fromType)) {
return value1;
} else if (value2.type.equals(fromType)) {
return value2;
} else {
// Should not happen
throw new IllegalStateException("Could not get 'from' rate");
}
}
protected Value getToRateValue(ValueType fromType) {
if (value1.type.equals(fromType)) {
return value2;
} else if (value2.type.equals(fromType)) {
return value1;
} else {
// Should not happen
throw new IllegalStateException("Could not get 'to' rate");
}
}
protected void checkIfValueTypeAvailable(ValueType type) {
checkArgument(value1.type.equals(type) || value2.type.equals(type),
"This exchange rate does not apply to: %s", type.getSymbol());
}
}