Extension of java.text.Format for locale-sensitive Bitcoin value formatting & parsing.

This commit is contained in:
Adam Mackler
2014-06-20 14:01:50 -04:00
committed by Mike Hearn
parent 887d6b0330
commit e2b802235d
4 changed files with 3492 additions and 0 deletions

View File

@@ -0,0 +1,205 @@
/*
* Copyright 2014 Adam Mackler
*
* 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.core.Coin.SMALLEST_UNIT_EXPONENT;
import com.google.common.collect.ImmutableList;
import java.math.BigInteger;
import static java.math.BigDecimal.ONE;
import static java.math.BigDecimal.ZERO;
import java.math.BigDecimal;
import static java.math.RoundingMode.HALF_UP;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
import java.util.Locale;
/**
* <p>This class, a concrete extension of {@link BtcFormat}, is distinguished by its
* accommodation of multiple denominational units as follows:
*
* <p>When formatting Bitcoin monetary values, an instance of this class automatically adjusts
* the denominational units in which it represents a given value so as to minimize the number
* of consecutive zeros in the number that is displayed, and includes either a currency code or
* symbol in the formatted value to indicate which denomination was chosen.
*
* <p>When parsing <code>String</code> representations of Bitcoin monetary values, instances of
* this class automatically recognize units indicators consisting of currency codes and
* symbols, including including those containing currency or metric prefixes such as
* <code>"¢"</code> or <code>"c"</code> to indicate hundredths, and interpret each number being
* parsed in accordance with the recognized denominational units.
*
* <p>A more detailed explanation, including examples, is in the documentation for the {@link
* BtcFormat} class, and further information beyond that is in the documentation for the {@link
* java.text.Format} class, from which this class descends.
* @see java.text.Format
* @see java.text.NumberFormat
* @see java.text.DecimalFormat
* @see DecimalFormatSymbols
* @see com.google.bitcoin.core.Coin
*/
public final class BtcAutoFormat extends BtcFormat {
/**
* Enum for specifying the style of currency indicators thas are used
* when formatting, ether codes or symbols.
*/
public enum Style {
/* Notes:
* 1) The odd-looking character in the replacements below, named "currency sign," is used in
* the patterns recognized by Java's number formatter. A single occurrence of this
* character specifies a currency symbol, while two adjacent occurrences indicate an
* international currency code.
* 2) The positive and negative patterns each have three parts: prefix, number, suffix.
* The number characters are limited to digits, zero, decimal-separator, group-separator, and
* scientific-notation specifier: [#0.,E]
* All number characters besides 'E' must be single-quoted in order to appear as
* literals in either the prefix or suffix.
* These patterns are explained in the documentation for java.text.DecimalFormat.
*/
/** Constant for the formatting style that uses a currency code, e.g., "BTC". */
CODE {
@Override void apply(DecimalFormat decimalFormat) {
/* To switch to using codes from symbols, we replace each single occurrence of the
* currency-sign character with two such characters in a row.
* We also insert a space character between every occurence of this character and an
* adjacent numerical digit or negative sign (that is, between the currency-sign and
* the signed-number). */
decimalFormat.applyPattern(
negify(decimalFormat.toPattern()).replaceAll("¤","¤¤").
replaceAll("([#0.,E-])¤¤","$1 ¤¤").
replaceAll("¤¤([0#.,E-])","¤¤ $1")
);
}
},
/** Constant for the formatting style that uses a currency symbol, e.g., "฿". */
SYMBOL {
@Override void apply(DecimalFormat decimalFormat) {
/* To make certain we are using symbols rather than codes, we replace
* each double occurrence of the currency sign character with a single. */
decimalFormat.applyPattern(negify(decimalFormat.toPattern()).replaceAll("¤¤","¤"));
}
};
/** Effect a style corresponding to an enum value on the given number formatter object. */
abstract void apply(DecimalFormat decimalFormat);
}
/** Constructor */
protected BtcAutoFormat(Locale locale, Style style, int fractionPlaces) {
super((DecimalFormat)NumberFormat.getCurrencyInstance(locale), fractionPlaces, ImmutableList.<Integer>of());
style.apply(this.numberFormat);
}
/**
* Calculate the appropriate denomination for the given Bitcoin monetary value. This
* method takes a BigInteger representing a quantity of satoshis, and returns the
* number of places that value's decimal point is to be moved when formatting said value
* in order that the resulting number represents the correct quantity of denominational
* units.
*
* <p>As a side-effect, this sets the units indicators of the underlying NumberFormat object.
* Only invoke this from a synchronized method, and be sure to put the DecimalFormatSymbols
* back to its proper state, otherwise immutability, equals() and hashCode() fail.
*/
@Override
protected int scale(BigInteger satoshis, int fractionPlaces) {
/* The algorithm is as follows. TODO: is there a way to optimize step 4?
1. Can we use coin denomination w/ no rounding? If yes, do it.
2. Else, can we use millicoin denomination w/ no rounding? If yes, do it.
3. Else, can we use micro denomination w/ no rounding? If yes, do it.
4. Otherwise we must round:
(a) round to nearest coin + decimals
(b) round to nearest millicoin + decimals
(c) round to nearest microcoin + decimals
Subtract each of (a), (b) and (c) from the true value, and choose the
denomination that gives smallest absolute difference. It case of tie, use the
smaller denomination.
*/
int places;
int coinOffset = Math.max(SMALLEST_UNIT_EXPONENT - fractionPlaces, 0);
BigDecimal inCoins = new BigDecimal(satoshis).movePointLeft(coinOffset);
if (inCoins.remainder(ONE).compareTo(ZERO) == 0) {
inCoins.setScale(0);
places = COIN_SCALE;
} else {
BigDecimal inMillis = inCoins.movePointRight(MILLICOIN_SCALE);
if (inMillis.remainder(ONE).compareTo(ZERO) == 0) {
inMillis.setScale(0);
places = MILLICOIN_SCALE;
} else {
BigDecimal inMicros = inCoins.movePointRight(MICROCOIN_SCALE);
if (inMicros.remainder(ONE).compareTo(ZERO) == 0) {
inMicros.setScale(0);
places = MICROCOIN_SCALE;
} else {
// no way to avoid rounding: so what denomination gives smallest error?
BigDecimal a = inCoins.subtract(inCoins.setScale(0, HALF_UP)).
movePointRight(coinOffset).abs();
BigDecimal b = inMillis.subtract(inMillis.setScale(0, HALF_UP)).
movePointRight(coinOffset - MILLICOIN_SCALE).abs();
BigDecimal c = inMicros.subtract(inMicros.setScale(0, HALF_UP)).
movePointRight(coinOffset - MICROCOIN_SCALE).abs();
if (a.compareTo(b) < 0)
if (a.compareTo(c) < 0) places = COIN_SCALE;
else places = MICROCOIN_SCALE;
else if (b.compareTo(c) < 0) places = MILLICOIN_SCALE;
else places = MICROCOIN_SCALE;
}
}
}
prefixUnitsIndicator(numberFormat, places);
return places;
}
/** Returns the <code>int</code> value indicating coin denomination. This is what causes
* the number in a parsed value that lacks a units indicator to be interpreted as a quantity
* of bitcoins. */
@Override
protected int scale() { return COIN_SCALE; }
/** Return the number of decimal places in the fraction part of numbers formatted by this
* instance. This is the maximum number of fraction places that will be displayed;
* the actual number used is limited to a precision of satoshis. */
public int fractionPlaces() { return minimumFractionDigits; }
/** Return true if the other instance is equivalent to this one.
* Formatters for different locales will never be equal, even
* if they behave identically. */
@Override public boolean equals(Object o) {
if (o == this) return true;
if (!(o instanceof BtcAutoFormat)) return false;
return super.equals((BtcAutoFormat)o);
}
/**
* Return a brief description of this formatter. The exact details of the representation
* are unspecified and subject to change, but will include some representation of the
* pattern and the number of fractional decimal places.
*/
@Override
public String toString() { return "Auto-format " + pattern(); }
}

View File

@@ -0,0 +1,191 @@
/*
* Copyright 2014 Adam Mackler
*
* 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.core.Coin.SMALLEST_UNIT_EXPONENT;
import static com.google.common.base.Preconditions.checkArgument;
import java.math.BigInteger;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
import java.util.Locale;
import java.util.List;
import java.util.ListIterator;
/**
* <p>This class, a concrete extension of {@link BtcFormat}, is distinguished in that each
* instance formats and by-default parses all Bitcoin monetary values in units of a single
* denomination that is specified at the time that instance is constructed.
*
* <p>By default, neither currency codes nor symbols are included in formatted values as
* output, nor recognized in parsed values as input. The can be overridden by applying a
* custom pattern using either the {@link BtcFormat.Builder#localizedPattern} or {@link BtcFormat.Builder#localizedPattern()} methods, as described in the documentation for the {@link BtcFormat.Builder}
* class.<ol>
*
* <p>A more detailed explanation, including examples, is in the documentation for the
* {@link BtcFormat} class, and further information beyond that is in the documentation for the
* {@link java.text.Format} class, from which this class descends.
* @see java.text.Format
* @see java.text.NumberFormat
* @see java.text.DecimalFormat
* @see com.google.bitcoin.core.Coin
*/
public final class BtcFixedFormat extends BtcFormat {
/** A constant specifying the use of as many optional decimal places in the fraction part
* of a formatted number as are useful for expressing precision. This value can be passed
* as the final argument to a factory method or {@link #format(Object, int, int...)}.
*/
public static final int[] REPEATING_PLACES = {1,1,1,1,1,1,1,1,1,1,1,1,1,1};
/** A constant specifying the use of as many optional groups of <strong>two</strong>
* decimal places in the fraction part of a formatted number as are useful for expressing
* precision. This value can be passed as the final argument to a factory method or
* {@link #format(Object, int, int...)}. */
public static final int[] REPEATING_DOUBLETS = {2,2,2,2,2,2,2};
/** A constant specifying the use of as many optional groups of <strong>three</strong>
* decimal places in the fraction part of a formatted number as are useful for expressing
* precision. This value can be passed as the final argument to a factory method or
* {@link #format(Object, int, int...)}. */
public static final int[] REPEATING_TRIPLETS = {3,3,3,3,3};
/** The number of places the decimal point of formatted values is shifted rightward from
* thet same value expressed in bitcoins. */
private final int scale;
/** Constructor */
protected BtcFixedFormat(
Locale locale, int scale, int minDecimals, List<Integer> groups
) {
super((DecimalFormat)NumberFormat.getInstance(locale), minDecimals, groups);
checkArgument(
scale <= SMALLEST_UNIT_EXPONENT,
"decimal cannot be shifted " + String.valueOf(scale) + " places"
);
this.scale = scale;
}
/** Return the decimal-place shift for this object's unit-denomination. For example, if
* the denomination is millibitcoins, this method will return the value <code>3</code>. As
* a side-effect, prefixes the currency signs of the underlying NumberFormat object. This
* method is invoked by the superclass when formatting. The arguments are ignored because
* the denomination is fixed regardless of the value being formatted.
*/
@Override
protected int scale(BigInteger satoshis, int fractionPlaces) {
prefixUnitsIndicator(numberFormat, scale);
return scale;
}
/** Return the decimal-place shift for this object's fixed unit-denomination. For example, if
* the denomination is millibitcoins, this method will return the value <code>3</code>. */
@Override
public int scale() { return scale; }
/**
* Return the currency code that identifies the units in which values formatted and
* (by-default) parsed by this instance are denominated. For example, if the formatter's
* denomination is millibitcoins, then this method will return <code>"mBTC"</code>,
* assuming the default base currency-code is not overridden using a
* {@link BtcFormat.Builder}. */
public String code() { return prefixCode(coinCode(), scale); }
/**
* Return the currency symbol that identifies the units in which values formatted by this
* instance are denominated. For example, when invoked on an instance denominated in
* millibitcoins, this method by default returns <code>"₥฿"</code>, depending on the
* locale. */
public String symbol() { return prefixSymbol(coinSymbol(), scale); }
/** Return the fractional decimal-placing used when formatting. This method returns an
* <code>int</code> array. The value of the first element is the minimum number of
* decimal places to be used in all cases, limited to a precision of satoshis. The value
* of each successive element is the size of an optional place-group that will be applied,
* possibly partially, if useful for expressing precision. The actual size of each group
* is limited to, and may be reduced to the limit of, a precision of no smaller than
* satoshis. */
public int[] fractionPlaceGroups() {
Object[] boxedArray = decimalGroups.toArray();
int len = boxedArray.length + 1;
int[] array = new int[len];
array[0] = minimumFractionDigits;
for (int i = 1; i < len; i++) { array[i] = (Integer) boxedArray[i-1]; }
return array;
}
/** Return true if the given object is equivalent to this one. Formatters for different
* locales will never be equal, even if they behave identically. */
@Override public boolean equals(Object o) {
if (o == this) return true;
if (!(o instanceof BtcFixedFormat)) return false;
BtcFixedFormat other = (BtcFixedFormat)o;
return other.scale() == scale() &&
other.decimalGroups.equals(decimalGroups) &&
super.equals(other);
}
/** Return a hash code value for this instance.
* @see java.lang.Object#hashCode
*/
@Override public int hashCode() {
int result = super.hashCode();
result = 31 * result + scale;
return result;
}
private static String prefixLabel(int scale) {
switch (scale) {
case COIN_SCALE: return "Coin-";
case 1: return "Decicoin-";
case 2: return "Centicoin-";
case MILLICOIN_SCALE: return "Millicoin-";
case MICROCOIN_SCALE: return "Microcoin-";
case -1: return "Dekacoin-";
case -2: return "Hectocoin-";
case -3: return "Kilocoin-";
case -6: return "Megacoin-";
default: return "Fixed (" + String.valueOf(scale) + ") ";
}
}
/**
* Returns a brief description of this formatter. The exact details of the representation
* are unspecified and subject to change, but will include some representation of the
* formatting/parsing pattern and the fractional decimal place grouping.
*/
@Override
public String toString() {
String label;
switch(scale) {
case COIN_SCALE:
label = "Coin-format";
break;
case MILLICOIN_SCALE:
label = "Millicoin-format";
break;
case MICROCOIN_SCALE:
label = "Microcoin-format";
break;
default: label = "Fixed (" + String.valueOf(scale) + ") format";
}
return prefixLabel(scale) + "format " + pattern();
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff