Introduce Fiat as a holder for fiat values. Coin and Fiat share an interface Monetary so that monetary infrastructure can be re-used.

Adapt CoinFormat to be used with Monetary. Add an ExchangeRate value class that can convert from Coin to Fiat and back.
This commit is contained in:
Andreas Schildbach
2014-07-19 17:58:36 +02:00
committed by Mike Hearn
parent 6deba7be92
commit 8e5ab9356c
8 changed files with 494 additions and 16 deletions

View File

@@ -27,18 +27,18 @@ import static com.google.common.base.Preconditions.checkArgument;
/**
* Represents a monetary Bitcoin value. This class is immutable.
*/
public final class Coin implements Comparable<Coin>, Serializable {
public final class Coin implements Monetary, Comparable<Coin>, Serializable {
/**
* Number of decimals for one Bitcoin. This constant is useful for quick adapting to other coins because a lot of
* constants derive from it.
*/
public static final int NUM_COIN_DECIMALS = 8;
public static final int SMALLEST_UNIT_EXPONENT = 8;
/**
* The number of satoshis equal to one bitcoin.
*/
private static final long COIN_VALUE = LongMath.pow(10, NUM_COIN_DECIMALS);
private static final long COIN_VALUE = LongMath.pow(10, SMALLEST_UNIT_EXPONENT);
/**
* Zero Bitcoins.
@@ -92,6 +92,19 @@ public final class Coin implements Comparable<Coin>, Serializable {
return new Coin(satoshis);
}
@Override
public int smallestUnitExponent() {
return SMALLEST_UNIT_EXPONENT;
}
/**
* Returns the number of satoshis of this monetary value.
*/
@Override
public long getValue() {
return value;
}
/**
* Convert an amount expressed in the way humans are used to into satoshis.
*/
@@ -113,7 +126,7 @@ public final class Coin implements Comparable<Coin>, Serializable {
* @throws IllegalArgumentException if you try to specify fractional satoshis, or a value out of range.
*/
public static Coin parseCoin(final String str) {
return Coin.valueOf(new BigDecimal(str).movePointRight(8).toBigIntegerExact().longValue());
return Coin.valueOf(new BigDecimal(str).movePointRight(SMALLEST_UNIT_EXPONENT).toBigIntegerExact().longValue());
}
public Coin add(final Coin value) {
@@ -188,6 +201,7 @@ public final class Coin implements Comparable<Coin>, Serializable {
return new Coin(this.value >> n);
}
@Override
public int signum() {
if (this.value == 0)
return 0;

View File

@@ -0,0 +1,38 @@
/**
* Copyright 2014 Andreas Schildbach
*
* 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 com.google.bitcoin.core;
import java.io.Serializable;
/**
* Classes implementing this interface represent a monetary value, such as a Bitcoin or fiat amount.
*/
public interface Monetary extends Serializable {
/**
* Returns the absolute value of exponent of the value of a "smallest unit" in scientific notation. For Bitcoin, a
* satoshi is worth 1E-8 so this would be 8.
*/
int smallestUnitExponent();
/**
* Returns the number of "smallest units" of this monetary value. For Bitcoin, this would be the number of satoshis.
*/
long getValue();
int signum();
}

View File

@@ -30,6 +30,7 @@ import java.util.Locale;
import java.util.Map;
import com.google.bitcoin.core.Coin;
import com.google.bitcoin.core.Monetary;
/**
* <p>
@@ -50,6 +51,8 @@ public final class CoinFormat {
public static final CoinFormat MBTC = new CoinFormat().shift(3).minDecimals(2).optionalDecimals(2);
/** Standard format for the µBTC denomination. */
public static final CoinFormat UBTC = new CoinFormat().shift(6).minDecimals(0).optionalDecimals(2);
/** Standard format for fiat amounts. */
public static final CoinFormat FIAT = new CoinFormat().shift(0).minDecimals(2).repeatOptionalDecimals(2, 1);
/** Currency code for base 1 Bitcoin. */
public static final String CODE_BTC = "BTC";
/** Currency code for base 1/1000 Bitcoin. */
@@ -295,26 +298,26 @@ public final class CoinFormat {
/**
* Format the given value to a human readable form.
*/
public CharSequence format(Coin coin) {
public CharSequence format(Monetary coin) {
// preparation
int maxDecimals = minDecimals;
if (decimalGroups != null)
for (int group : decimalGroups)
maxDecimals += group;
checkState(maxDecimals <= Coin.NUM_COIN_DECIMALS);
checkState(maxDecimals <= coin.smallestUnitExponent());
// rounding
long satoshis = Math.abs(coin.value);
long precisionDivisor = checkedPow(10, Coin.NUM_COIN_DECIMALS - shift - maxDecimals);
long satoshis = Math.abs(coin.getValue());
long precisionDivisor = checkedPow(10, coin.smallestUnitExponent() - shift - maxDecimals);
satoshis = checkedMultiply(divide(satoshis, precisionDivisor, roundingMode), precisionDivisor);
// shifting
long shiftDivisor = checkedPow(10, Coin.NUM_COIN_DECIMALS - shift);
long shiftDivisor = checkedPow(10, coin.smallestUnitExponent() - shift);
long numbers = satoshis / shiftDivisor;
long decimals = satoshis % shiftDivisor;
// formatting
String decimalsStr = String.format(Locale.US, "%0" + (Coin.NUM_COIN_DECIMALS - shift) + "d", decimals);
String decimalsStr = String.format(Locale.US, "%0" + (coin.smallestUnitExponent() - shift) + "d", decimals);
StringBuilder str = new StringBuilder(decimalsStr);
while (str.length() > minDecimals && str.charAt(str.length() - 1) == '0')
str.setLength(str.length() - 1); // trim trailing zero
@@ -332,7 +335,7 @@ public final class CoinFormat {
if (str.length() > 0)
str.insert(0, decimalMark);
str.insert(0, numbers);
if (coin.value < 0)
if (coin.getValue() < 0)
str.insert(0, negativeSign);
else if (positiveSign != 0)
str.insert(0, positiveSign);
@@ -355,7 +358,21 @@ public final class CoinFormat {
* if the string cannot be parsed for some reason
*/
public Coin parse(String str) throws NumberFormatException {
checkState(DECIMALS_PADDING.length() >= Coin.NUM_COIN_DECIMALS);
return Coin.valueOf(parseValue(str, Coin.SMALLEST_UNIT_EXPONENT));
}
/**
* Parse a human readable fiat value to a {@link com.google.bitcoin.core.Fiat} instance.
*
* @throws NumberFormatException
* if the string cannot be parsed for some reason
*/
public Fiat parseFiat(String currencyCode, String str) throws NumberFormatException {
return Fiat.valueOf(currencyCode, parseValue(str, Fiat.SMALLEST_UNIT_EXPONENT));
}
private long parseValue(String str, int smallestUnitExponent) {
checkState(DECIMALS_PADDING.length() >= smallestUnitExponent);
if (str.isEmpty())
throw new NumberFormatException("empty string");
char first = str.charAt(0);
@@ -373,14 +390,14 @@ public final class CoinFormat {
numbers = str;
decimals = DECIMALS_PADDING;
}
String satoshis = numbers + decimals.substring(0, Coin.NUM_COIN_DECIMALS - shift);
String satoshis = numbers + decimals.substring(0, smallestUnitExponent - shift);
for (char c : satoshis.toCharArray())
if (!Character.isDigit(c))
throw new NumberFormatException("illegal character: " + c);
Coin coin = Coin.valueOf(Long.parseLong(satoshis));
long value = Long.parseLong(satoshis);
if (first == negativeSign)
coin = coin.negate();
return coin;
value = -value;
return value;
}
/**

View File

@@ -0,0 +1,65 @@
/**
* Copyright 2014 Andreas Schildbach
*
* 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 com.google.bitcoin.utils;
import static com.google.common.base.Preconditions.checkArgument;
import java.io.Serializable;
import java.math.BigInteger;
import com.google.bitcoin.core.Coin;
/**
* An exchange rate is expressed as a ratio of a {@link Coin} and a {@link Fiat} amount.
*/
public class ExchangeRate implements Serializable {
public final Coin coin;
public final Fiat fiat;
public ExchangeRate(Coin coin, Fiat fiat) {
this.coin = coin;
this.fiat = fiat;
}
public ExchangeRate(Fiat fiat) {
this.coin = Coin.COIN;
this.fiat = fiat;
}
public Fiat coinToFiat(Coin convertCoin) {
// Use BigInteger because it's much easier to maintain full precision without overflowing.
final BigInteger converted = BigInteger.valueOf(convertCoin.value).multiply(BigInteger.valueOf(fiat.value))
.divide(BigInteger.valueOf(coin.value));
if (converted.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0
|| converted.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0)
throw new ArithmeticException("Overflow");
return Fiat.valueOf(fiat.currencyCode, converted.longValue());
}
public Coin fiatToCoin(Fiat convertFiat) {
checkArgument(convertFiat.currencyCode.equals(fiat.currencyCode), "Currency mismatch: %s vs %s",
convertFiat.currencyCode, fiat.currencyCode);
// Use BigInteger because it's much easier to maintain full precision without overflowing.
final BigInteger converted = BigInteger.valueOf(convertFiat.value).multiply(BigInteger.valueOf(coin.value))
.divide(BigInteger.valueOf(fiat.value));
if (converted.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0
|| converted.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0)
throw new ArithmeticException("Overflow");
return Coin.valueOf(converted.longValue());
}
}

View File

@@ -0,0 +1,225 @@
/**
* Copyright 2014 Andreas Schildbach
*
* 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 com.google.bitcoin.utils;
import static com.google.common.base.Preconditions.checkArgument;
import java.io.Serializable;
import java.math.BigDecimal;
import com.google.bitcoin.core.Monetary;
import com.google.common.math.LongMath;
/**
* Represents a monetary fiat value. It was decided to not fold this into {@link Coin} because of type safety. Fiat
* values always come with an attached currency code.
*
* This class is immutable.
*/
public final class Fiat implements Monetary, Comparable<Fiat>, Serializable {
/**
* The absolute value of exponent of the value of a "smallest unit" in scientific notation. We picked 4 rather than
* 2, because in financial applications it's common to use sub-cent precision.
*/
public static final int SMALLEST_UNIT_EXPONENT = 4;
/**
* The number of smallest units of this monetary value.
*/
public final long value;
public final String currencyCode;
private Fiat(final String currencyCode, final long value) {
this.value = value;
this.currencyCode = currencyCode;
}
public static Fiat valueOf(final String currencyCode, final long value) {
return new Fiat(currencyCode, value);
}
@Override
public int smallestUnitExponent() {
return SMALLEST_UNIT_EXPONENT;
}
/**
* Returns the number of "smallest units" of this monetary value.
*/
@Override
public long getValue() {
return value;
}
public String getCurrencyCode() {
return currencyCode;
}
/**
* Parses an amount expressed in the way humans are used to.
* <p>
* <p/>
* This takes string in a format understood by {@link BigDecimal#BigDecimal(String)}, for example "0", "1", "0.10",
* "1.23E3", "1234.5E-5".
*
* @throws IllegalArgumentException
* if you try to specify fractional satoshis, or a value out of range.
*/
public static Fiat parseFiat(final String currencyCode, final String str) {
return Fiat.valueOf(currencyCode, new BigDecimal(str).movePointRight(SMALLEST_UNIT_EXPONENT)
.toBigIntegerExact().longValue());
}
public Fiat add(final Fiat value) {
checkArgument(value.currencyCode.equals(currencyCode));
return new Fiat(currencyCode, LongMath.checkedAdd(this.value, value.value));
}
public Fiat subtract(final Fiat value) {
checkArgument(value.currencyCode.equals(currencyCode));
return new Fiat(currencyCode, LongMath.checkedSubtract(this.value, value.value));
}
public Fiat multiply(final long factor) {
return new Fiat(currencyCode, LongMath.checkedMultiply(this.value, factor));
}
public Fiat divide(final long divisor) {
return new Fiat(currencyCode, this.value / divisor);
}
public Fiat[] divideAndRemainder(final long divisor) {
return new Fiat[] { new Fiat(currencyCode, this.value / divisor), new Fiat(currencyCode, this.value % divisor) };
}
public long divide(final Fiat divisor) {
checkArgument(divisor.currencyCode.equals(currencyCode));
return this.value / divisor.value;
}
/**
* Returns true if and only if this instance represents a monetary value greater than zero, otherwise false.
*/
public boolean isPositive() {
return signum() == 1;
}
/**
* Returns true if and only if this instance represents a monetary value less than zero, otherwise false.
*/
public boolean isNegative() {
return signum() == -1;
}
/**
* Returns true if and only if this instance represents zero monetary value, otherwise false.
*/
public boolean isZero() {
return signum() == 0;
}
/**
* Returns true if the monetary value represented by this instance is greater than that of the given other Coin,
* otherwise false.
*/
public boolean isGreaterThan(Fiat other) {
return compareTo(other) > 0;
}
/**
* Returns true if the monetary value represented by this instance is less than that of the given other Coin,
* otherwise false.
*/
public boolean isLessThan(Fiat other) {
return compareTo(other) < 0;
}
@Override
public int signum() {
if (this.value == 0)
return 0;
return this.value < 0 ? -1 : 1;
}
public Fiat negate() {
return new Fiat(currencyCode, -this.value);
}
/**
* Returns the number of satoshis of this monetary value. It's deprecated in favour of accessing {@link #value}
* directly.
*/
public long longValue() {
return this.value;
}
private static final CoinFormat FRIENDLY_FORMAT = CoinFormat.FIAT.postfixCode();
/**
* Returns the value as a 0.12 type string. More digits after the decimal place will be used if necessary, but two
* will always be present.
*/
public String toFriendlyString() {
return FRIENDLY_FORMAT.code(0, currencyCode).format(this).toString();
}
private static final CoinFormat PLAIN_FORMAT = CoinFormat.FIAT.minDecimals(0).repeatOptionalDecimals(1, 4).noCode();
/**
* <p>
* Returns the value as a plain string denominated in BTC. The result is unformatted with no trailing zeroes. For
* instance, a value of 150000 satoshis gives an output string of "0.0015" BTC
* </p>
*/
public String toPlainString() {
return PLAIN_FORMAT.format(this).toString();
}
@Override
public String toString() {
return Long.toString(value);
}
@Override
public boolean equals(final Object o) {
if (o == this)
return true;
if (o == null || o.getClass() != getClass())
return false;
final Fiat other = (Fiat) o;
if (this.value != other.value)
return false;
if (!this.currencyCode.equals(other.currencyCode))
return false;
return true;
}
@Override
public int hashCode() {
return (int) this.value + 37 * this.currencyCode.hashCode();
}
@Override
public int compareTo(final Fiat other) {
if (!this.currencyCode.equals(other.currencyCode))
return this.currencyCode.compareTo(other.currencyCode);
if (this.value != other.value)
return this.value > other.value ? 1 : -1;
return 0;
}
}

View File

@@ -306,4 +306,11 @@ public class CoinFormatTest {
public void parseInvalidHugeNegativeNumber() throws Exception {
NO_CODE.parse("-99999999999999999999");
}
private static final Fiat ONE_EURO = Fiat.parseFiat("EUR", "1");
@Test
public void fiat() throws Exception {
assertEquals(ONE_EURO, NO_CODE.parseFiat("EUR", "1"));
}
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2014 Andreas Schildbach
*
* 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 com.google.bitcoin.utils;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
import com.google.bitcoin.core.Coin;
public class ExchangeRateTest {
@Test
public void normalRate() throws Exception {
ExchangeRate rate = new ExchangeRate(Fiat.parseFiat("EUR", "500"));
assertEquals("0.5", rate.coinToFiat(Coin.MILLICOIN).toPlainString());
assertEquals("0.002", rate.fiatToCoin(Fiat.parseFiat("EUR", "1")).toPlainString());
}
@Test
public void bigRate() throws Exception {
ExchangeRate rate = new ExchangeRate(Coin.parseCoin("0.0001"), Fiat.parseFiat("BYR", "5320387.3"));
assertEquals("53203873000", rate.coinToFiat(Coin.COIN).toPlainString());
assertEquals("0", rate.fiatToCoin(Fiat.parseFiat("BYR", "1")).toPlainString()); // Tiny value!
}
@Test
public void smallRate() throws Exception {
ExchangeRate rate = new ExchangeRate(Coin.parseCoin("1000"), Fiat.parseFiat("XXX", "0.0001"));
assertEquals("0", rate.coinToFiat(Coin.COIN).toPlainString()); // Tiny value!
assertEquals("10000000", rate.fiatToCoin(Fiat.parseFiat("XXX", "1")).toPlainString());
}
@Test(expected = IllegalArgumentException.class)
public void currencyCodeMismatch() throws Exception {
ExchangeRate rate = new ExchangeRate(Fiat.parseFiat("EUR", "500"));
rate.fiatToCoin(Fiat.parseFiat("USD", "1"));
}
}

View File

@@ -0,0 +1,59 @@
/**
* Copyright 2014 Andreas Schildbach
*
* 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 com.google.bitcoin.utils;
import static com.google.bitcoin.utils.Fiat.parseFiat;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
public class FiatTest {
@Test
public void testParseAndValueOf() {
assertEquals(Fiat.valueOf("EUR", 10000), parseFiat("EUR", "1"));
assertEquals(Fiat.valueOf("EUR", 100), parseFiat("EUR", "0.01"));
assertEquals(Fiat.valueOf("EUR", 1), parseFiat("EUR", "0.0001"));
assertEquals(Fiat.valueOf("EUR", -10000), parseFiat("EUR", "-1"));
}
@Test
public void testToFriendlyString() {
assertEquals("1.00 EUR", parseFiat("EUR", "1").toFriendlyString());
assertEquals("1.23 EUR", parseFiat("EUR", "1.23").toFriendlyString());
assertEquals("0.0010 EUR", parseFiat("EUR", "0.001").toFriendlyString());
assertEquals("-1.23 EUR", parseFiat("EUR", "-1.23").toFriendlyString());
}
@Test
public void testToPlainString() {
assertEquals("0.0015", Fiat.valueOf("EUR", 15).toPlainString());
assertEquals("1.23", parseFiat("EUR", "1.23").toPlainString());
assertEquals("0.1", parseFiat("EUR", "0.1").toPlainString());
assertEquals("1.1", parseFiat("EUR", "1.1").toPlainString());
assertEquals("21.12", parseFiat("EUR", "21.12").toPlainString());
assertEquals("321.123", parseFiat("EUR", "321.123").toPlainString());
assertEquals("4321.1234", parseFiat("EUR", "4321.1234").toPlainString());
// check there are no trailing zeros
assertEquals("1", parseFiat("EUR", "1.0").toPlainString());
assertEquals("2", parseFiat("EUR", "2.00").toPlainString());
assertEquals("3", parseFiat("EUR", "3.000").toPlainString());
assertEquals("4", parseFiat("EUR", "4.0000").toPlainString());
}
}