org.bitcoinj.utils.MonetaryFormat Maven / Gradle / Ivy
/*
* 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 org.bitcoinj.utils;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.math.LongMath.checkedMultiply;
import static com.google.common.math.LongMath.checkedPow;
import static com.google.common.math.LongMath.divide;
import java.math.RoundingMode;
import java.text.DecimalFormatSymbols;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.Monetary;
/**
*
* Utility for formatting and parsing coin values to and from human readable form.
*
*
*
* MonetaryFormat instances are immutable. Invoking a configuration method has no effect on the receiving instance; you
* must store and use the new instance it returns, instead. Instances are thread safe, so they may be stored safely as
* static constants.
*
*/
public final class MonetaryFormat {
/** Standard format for the BTC denomination. */
public static final MonetaryFormat BTC = new MonetaryFormat().shift(0).minDecimals(2).repeatOptionalDecimals(2, 3);
/** Standard format for the mBTC denomination. */
public static final MonetaryFormat MBTC = new MonetaryFormat().shift(3).minDecimals(2).optionalDecimals(2);
/** Standard format for the µBTC denomination. */
public static final MonetaryFormat UBTC = new MonetaryFormat().shift(6).minDecimals(0).optionalDecimals(2);
/** Standard format for fiat amounts. */
public static final MonetaryFormat FIAT = new MonetaryFormat().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. */
public static final String CODE_MBTC = "mBTC";
/** Currency code for base 1/1000000 Bitcoin. */
public static final String CODE_UBTC = "µBTC";
public static final int MAX_DECIMALS = 8;
private final char negativeSign;
private final char positiveSign;
private final char zeroDigit;
private final char decimalMark;
private final int minDecimals;
private final List decimalGroups;
private final int shift;
private final RoundingMode roundingMode;
private final String[] codes;
private final char codeSeparator;
private final boolean codePrefixed;
private static final String DECIMALS_PADDING = "0000000000000000"; // a few more than necessary for Bitcoin
/**
* Set character to prefix negative values.
*/
public MonetaryFormat negativeSign(char negativeSign) {
checkArgument(!Character.isDigit(negativeSign));
checkArgument(negativeSign > 0);
if (negativeSign == this.negativeSign)
return this;
else
return new MonetaryFormat(negativeSign, positiveSign, zeroDigit, decimalMark, minDecimals, decimalGroups,
shift, roundingMode, codes, codeSeparator, codePrefixed);
}
/**
* Set character to prefix positive values. A zero value means no sign is used in this case. For parsing, a missing
* sign will always be interpreted as if the positive sign was used.
*/
public MonetaryFormat positiveSign(char positiveSign) {
checkArgument(!Character.isDigit(positiveSign));
if (positiveSign == this.positiveSign)
return this;
else
return new MonetaryFormat(negativeSign, positiveSign, zeroDigit, decimalMark, minDecimals, decimalGroups,
shift, roundingMode, codes, codeSeparator, codePrefixed);
}
/**
* Set character range to use for representing digits. It starts with the specified character representing zero.
*/
public MonetaryFormat digits(char zeroDigit) {
if (zeroDigit == this.zeroDigit)
return this;
else
return new MonetaryFormat(negativeSign, positiveSign, zeroDigit, decimalMark, minDecimals, decimalGroups,
shift, roundingMode, codes, codeSeparator, codePrefixed);
}
/**
* Set character to use as the decimal mark. If the formatted value does not have any decimals, no decimal mark is
* used either.
*/
public MonetaryFormat decimalMark(char decimalMark) {
checkArgument(!Character.isDigit(decimalMark));
checkArgument(decimalMark > 0);
if (decimalMark == this.decimalMark)
return this;
else
return new MonetaryFormat(negativeSign, positiveSign, zeroDigit, decimalMark, minDecimals, decimalGroups,
shift, roundingMode, codes, codeSeparator, codePrefixed);
}
/**
* Set minimum number of decimals to use for formatting. If the value precision exceeds all decimals specified
* (including additional decimals specified by {@link #optionalDecimals(int...)} or
* {@link #repeatOptionalDecimals(int, int)}), the value will be rounded. This configuration is not relevant for
* parsing.
*/
public MonetaryFormat minDecimals(int minDecimals) {
if (minDecimals == this.minDecimals)
return this;
else
return new MonetaryFormat(negativeSign, positiveSign, zeroDigit, decimalMark, minDecimals, decimalGroups,
shift, roundingMode, codes, codeSeparator, codePrefixed);
}
/**
*
* Set additional groups of decimals to use after the minimum decimals, if they are useful for expressing precision.
* Each value is a number of decimals in that group. If the value precision exceeds all decimals specified
* (including minimum decimals), the value will be rounded. This configuration is not relevant for parsing.
*
*
*
* For example, if you pass 4,2 it will add four decimals to your formatted string if needed, and then add
* another two decimals if needed. At this point, rather than adding further decimals the value will be rounded.
*
*
* @param groups
* any number numbers of decimals, one for each group
*/
public MonetaryFormat optionalDecimals(int... groups) {
List decimalGroups = new ArrayList<>(groups.length);
for (int group : groups)
decimalGroups.add(group);
return new MonetaryFormat(negativeSign, positiveSign, zeroDigit, decimalMark, minDecimals, decimalGroups,
shift, roundingMode, codes, codeSeparator, codePrefixed);
}
/**
*
* Set repeated additional groups of decimals to use after the minimum decimals, if they are useful for expressing
* precision. If the value precision exceeds all decimals specified (including minimum decimals), the value will be
* rounded. This configuration is not relevant for parsing.
*
*
*
* For example, if you pass 1,8 it will up to eight decimals to your formatted string if needed. After
* these have been used up, rather than adding further decimals the value will be rounded.
*
*
* @param decimals
* value of the group to be repeated
* @param repetitions
* number of repetitions
*/
public MonetaryFormat repeatOptionalDecimals(int decimals, int repetitions) {
checkArgument(repetitions >= 0);
List decimalGroups = new ArrayList<>(repetitions);
for (int i = 0; i < repetitions; i++)
decimalGroups.add(decimals);
return new MonetaryFormat(negativeSign, positiveSign, zeroDigit, decimalMark, minDecimals, decimalGroups,
shift, roundingMode, codes, codeSeparator, codePrefixed);
}
/**
* Set number of digits to shift the decimal separator to the right, coming from the standard BTC notation that was
* common pre-2014. Note this will change the currency code if enabled.
*/
public MonetaryFormat shift(int shift) {
if (shift == this.shift)
return this;
else
return new MonetaryFormat(negativeSign, positiveSign, zeroDigit, decimalMark, minDecimals, decimalGroups,
shift, roundingMode, codes, codeSeparator, codePrefixed);
}
/**
* Set rounding mode to use when it becomes necessary.
*/
public MonetaryFormat roundingMode(RoundingMode roundingMode) {
if (roundingMode == this.roundingMode)
return this;
else
return new MonetaryFormat(negativeSign, positiveSign, zeroDigit, decimalMark, minDecimals, decimalGroups,
shift, roundingMode, codes, codeSeparator, codePrefixed);
}
/**
* Don't display currency code when formatting. This configuration is not relevant for parsing.
*/
public MonetaryFormat noCode() {
if (codes == null)
return this;
else
return new MonetaryFormat(negativeSign, positiveSign, zeroDigit, decimalMark, minDecimals, decimalGroups,
shift, roundingMode, null, codeSeparator, codePrefixed);
}
/**
* Configure currency code for given decimal separator shift. This configuration is not relevant for parsing.
*
* @param codeShift
* decimal separator shift, see {@link #shift}
* @param code
* currency code
*/
public MonetaryFormat code(int codeShift, String code) {
checkArgument(codeShift >= 0);
final String[] codes = null == this.codes
? new String[MAX_DECIMALS]
: Arrays.copyOf(this.codes, this.codes.length);
codes[codeShift] = code;
return new MonetaryFormat(negativeSign, positiveSign, zeroDigit, decimalMark, minDecimals, decimalGroups,
shift, roundingMode, codes, codeSeparator, codePrefixed);
}
/**
* Separator between currency code and formatted value. This configuration is not relevant for parsing.
*/
public MonetaryFormat codeSeparator(char codeSeparator) {
checkArgument(!Character.isDigit(codeSeparator));
checkArgument(codeSeparator > 0);
if (codeSeparator == this.codeSeparator)
return this;
else
return new MonetaryFormat(negativeSign, positiveSign, zeroDigit, decimalMark, minDecimals, decimalGroups,
shift, roundingMode, codes, codeSeparator, codePrefixed);
}
/**
* Prefix formatted output by currency code. This configuration is not relevant for parsing.
*/
public MonetaryFormat prefixCode() {
if (codePrefixed)
return this;
else
return new MonetaryFormat(negativeSign, positiveSign, zeroDigit, decimalMark, minDecimals, decimalGroups,
shift, roundingMode, codes, codeSeparator, true);
}
/**
* Postfix formatted output with currency code. This configuration is not relevant for parsing.
*/
public MonetaryFormat postfixCode() {
if (!codePrefixed)
return this;
else
return new MonetaryFormat(negativeSign, positiveSign, zeroDigit, decimalMark, minDecimals, decimalGroups,
shift, roundingMode, codes, codeSeparator, false);
}
/**
* Configure this instance with values from a {@link Locale}.
*/
public MonetaryFormat withLocale(Locale locale) {
DecimalFormatSymbols dfs = new DecimalFormatSymbols(locale);
char negativeSign = dfs.getMinusSign();
char zeroDigit = dfs.getZeroDigit();
char decimalMark = dfs.getMonetaryDecimalSeparator();
return new MonetaryFormat(negativeSign, positiveSign, zeroDigit, decimalMark, minDecimals, decimalGroups,
shift, roundingMode, codes, codeSeparator, codePrefixed);
}
public MonetaryFormat() {
// defaults
this.negativeSign = '-';
this.positiveSign = 0; // none
this.zeroDigit = '0';
this.decimalMark = '.';
this.minDecimals = 2;
this.decimalGroups = null;
this.shift = 0;
this.roundingMode = RoundingMode.HALF_UP;
this.codes = new String[MAX_DECIMALS];
this.codes[0] = CODE_BTC;
this.codes[3] = CODE_MBTC;
this.codes[6] = CODE_UBTC;
this.codeSeparator = ' ';
this.codePrefixed = true;
}
private MonetaryFormat(char negativeSign, char positiveSign, char zeroDigit, char decimalMark, int minDecimals,
List decimalGroups, int shift, RoundingMode roundingMode, String[] codes,
char codeSeparator, boolean codePrefixed) {
this.negativeSign = negativeSign;
this.positiveSign = positiveSign;
this.zeroDigit = zeroDigit;
this.decimalMark = decimalMark;
this.minDecimals = minDecimals;
this.decimalGroups = decimalGroups;
this.shift = shift;
this.roundingMode = roundingMode;
this.codes = codes;
this.codeSeparator = codeSeparator;
this.codePrefixed = codePrefixed;
}
/**
* Format the given monetary value to a human readable form.
*/
public CharSequence format(Monetary monetary) {
// preparation
int maxDecimals = minDecimals;
if (decimalGroups != null)
for (int group : decimalGroups)
maxDecimals += group;
int smallestUnitExponent = monetary.smallestUnitExponent();
checkState(maxDecimals <= smallestUnitExponent,
"The maximum possible number of decimals (%s) cannot exceed %s.", maxDecimals, smallestUnitExponent);
// rounding
long satoshis = Math.abs(monetary.getValue());
long precisionDivisor = checkedPow(10, smallestUnitExponent - shift - maxDecimals);
satoshis = checkedMultiply(divide(satoshis, precisionDivisor, roundingMode), precisionDivisor);
// shifting
long shiftDivisor = checkedPow(10, smallestUnitExponent - shift);
long numbers = satoshis / shiftDivisor;
long decimals = satoshis % shiftDivisor;
// formatting
String decimalsStr = String.format(Locale.US, "%0" + (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
int i = minDecimals;
if (decimalGroups != null) {
for (int group : decimalGroups) {
if (str.length() > i && str.length() < i + group) {
while (str.length() < i + group)
str.append('0');
break;
}
i += group;
}
}
if (str.length() > 0)
str.insert(0, decimalMark);
str.insert(0, numbers);
if (monetary.getValue() < 0)
str.insert(0, negativeSign);
else if (positiveSign != 0)
str.insert(0, positiveSign);
if (codes != null) {
if (codePrefixed) {
str.insert(0, codeSeparator);
str.insert(0, code());
} else {
str.append(codeSeparator);
str.append(code());
}
}
// Convert to non-arabic digits.
if (zeroDigit != '0') {
int offset = zeroDigit - '0';
for (int d = 0; d < str.length(); d++) {
char c = str.charAt(d);
if (Character.isDigit(c))
str.setCharAt(d, (char) (c + offset));
}
}
return str;
}
/**
* Parse a human readable coin value to a {@link org.bitcoinj.core.Coin} instance.
*
* @throws NumberFormatException
* if the string cannot be parsed for some reason
*/
public Coin parse(String str) throws NumberFormatException {
return Coin.valueOf(parseValue(str, Coin.SMALLEST_UNIT_EXPONENT));
}
/**
* Parse a human readable fiat value to a {@link org.bitcoinj.utils.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);
if (first == negativeSign || first == positiveSign)
str = str.substring(1);
String numbers;
String decimals;
int decimalMarkIndex = str.indexOf(decimalMark);
if (decimalMarkIndex != -1) {
numbers = str.substring(0, decimalMarkIndex);
decimals = (str + DECIMALS_PADDING).substring(decimalMarkIndex + 1);
if (decimals.indexOf(decimalMark) != -1)
throw new NumberFormatException("more than one decimal mark");
} else {
numbers = str;
decimals = DECIMALS_PADDING;
}
String satoshis = numbers + decimals.substring(0, smallestUnitExponent - shift);
for (char c : satoshis.toCharArray())
if (!Character.isDigit(c))
throw new NumberFormatException("illegal character: " + c);
long value = Long.parseLong(satoshis); // Non-arabic digits allowed here.
if (first == negativeSign)
value = -value;
return value;
}
/**
* Get currency code that will be used for current shift.
*/
public String code() {
if (codes == null)
return null;
if (codes[shift] == null)
throw new NumberFormatException("missing code for shift: " + shift);
return codes[shift];
}
}