mirror of
https://github.com/Qortal/altcoinj.git
synced 2025-11-02 05:27:17 +00:00
Extension of java.text.Format for locale-sensitive Bitcoin value formatting & parsing.
This commit is contained in:
205
core/src/main/java/com/google/bitcoin/utils/BtcAutoFormat.java
Normal file
205
core/src/main/java/com/google/bitcoin/utils/BtcAutoFormat.java
Normal 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(); }
|
||||
|
||||
}
|
||||
191
core/src/main/java/com/google/bitcoin/utils/BtcFixedFormat.java
Normal file
191
core/src/main/java/com/google/bitcoin/utils/BtcFixedFormat.java
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
1599
core/src/main/java/com/google/bitcoin/utils/BtcFormat.java
Normal file
1599
core/src/main/java/com/google/bitcoin/utils/BtcFormat.java
Normal file
File diff suppressed because it is too large
Load Diff
1497
core/src/test/java/com/google/bitcoin/utils/BtcFormatTest.java
Normal file
1497
core/src/test/java/com/google/bitcoin/utils/BtcFormatTest.java
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user