org.bitcoinj.utils.BtcAutoFormat Maven / Gradle / Ivy
/*
* 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 org.bitcoinj.utils;
import static org.bitcoinj.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;
/**
* This class, a concrete extension of {@link BtcFormat}, is distinguished by its
* accommodation of multiple denominational units as follows:
*
*
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.
*
*
When parsing String
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
* "¢"
or "c"
to indicate hundredths, and interpret each number being
* parsed in accordance with the recognized denominational units.
*
*
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 org.bitcoinj.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.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.
*
* 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) {
places = COIN_SCALE;
} else {
BigDecimal inMillis = inCoins.movePointRight(MILLICOIN_SCALE);
if (inMillis.remainder(ONE).compareTo(ZERO) == 0) {
places = MILLICOIN_SCALE;
} else {
BigDecimal inMicros = inCoins.movePointRight(MICROCOIN_SCALE);
if (inMicros.remainder(ONE).compareTo(ZERO) == 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 int
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 (this == o) return true;
if (!(o instanceof BtcAutoFormat)) return false;
return super.equals(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(); }
}