From e2b802235d722ec9d23beb279238bf74e81a57d8 Mon Sep 17 00:00:00 2001 From: Adam Mackler Date: Fri, 20 Jun 2014 14:01:50 -0400 Subject: [PATCH] Extension of java.text.Format for locale-sensitive Bitcoin value formatting & parsing. --- .../google/bitcoin/utils/BtcAutoFormat.java | 205 +++ .../google/bitcoin/utils/BtcFixedFormat.java | 191 ++ .../com/google/bitcoin/utils/BtcFormat.java | 1599 +++++++++++++++++ .../google/bitcoin/utils/BtcFormatTest.java | 1497 +++++++++++++++ 4 files changed, 3492 insertions(+) create mode 100644 core/src/main/java/com/google/bitcoin/utils/BtcAutoFormat.java create mode 100644 core/src/main/java/com/google/bitcoin/utils/BtcFixedFormat.java create mode 100644 core/src/main/java/com/google/bitcoin/utils/BtcFormat.java create mode 100644 core/src/test/java/com/google/bitcoin/utils/BtcFormatTest.java diff --git a/core/src/main/java/com/google/bitcoin/utils/BtcAutoFormat.java b/core/src/main/java/com/google/bitcoin/utils/BtcAutoFormat.java new file mode 100644 index 00000000..b687f099 --- /dev/null +++ b/core/src/main/java/com/google/bitcoin/utils/BtcAutoFormat.java @@ -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; + +/** + *

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 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.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) { + 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 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 (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(); } + +} diff --git a/core/src/main/java/com/google/bitcoin/utils/BtcFixedFormat.java b/core/src/main/java/com/google/bitcoin/utils/BtcFixedFormat.java new file mode 100644 index 00000000..e669bf10 --- /dev/null +++ b/core/src/main/java/com/google/bitcoin/utils/BtcFixedFormat.java @@ -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; + +/** + *

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. + * + *

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.

    + * + *

    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 two + * 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 three + * 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 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 3. 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 3. */ + @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 "mBTC", + * 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 "₥฿", depending on the + * locale. */ + public String symbol() { return prefixSymbol(coinSymbol(), scale); } + + /** Return the fractional decimal-placing used when formatting. This method returns an + * int 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(); + } + +} diff --git a/core/src/main/java/com/google/bitcoin/utils/BtcFormat.java b/core/src/main/java/com/google/bitcoin/utils/BtcFormat.java new file mode 100644 index 00000000..3e436702 --- /dev/null +++ b/core/src/main/java/com/google/bitcoin/utils/BtcFormat.java @@ -0,0 +1,1599 @@ +/* + * 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 com.google.bitcoin.utils.BtcAutoFormat.Style; +import static com.google.bitcoin.utils.BtcAutoFormat.Style.*; + +import com.google.bitcoin.core.Coin; +import com.google.common.collect.ImmutableList; +import static com.google.common.base.Preconditions.checkArgument; +import com.google.common.base.Strings; + +import java.math.BigDecimal; +import java.math.BigInteger; +import static java.math.BigDecimal.ONE; +import static java.math.BigDecimal.ZERO; +import static java.math.RoundingMode.HALF_UP; + +import java.text.AttributedCharacterIterator; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.text.FieldPosition; +import java.text.Format; +import java.text.NumberFormat; +import java.text.ParseException; +import java.text.ParsePosition; + +import java.util.Locale; +import java.util.List; +import java.util.ArrayList; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + *

    Instances of this class format and parse locale-specific numerical + * representations of Bitcoin monetary values. + * + *

    A primary goal of this class is to minimize the danger of + * human-misreading of monetary values due to mis-counting the number + * of zeros (or, more generally, of decimal places) in the number that + * represents a Bitcoin monetary value. Some of the features offered for doing this + * are:

      + *
    1. automatic adjustment of denominational units in which a + * value is represented so as to lessen the number of adjacent zeros, + *
    2. use of locale-specific decimal-separators to group digits in + * the integer portion of formatted numbers, + *
    3. fine control over the number and grouping of fractional decimal places, and + *
    4. access to character information that allows for vertical + * alignment of tabular columns of formatted values.
    + * + *

    Basic Usage

    + * + * Basic usage is very simple:
      + *
    1. Construct a new formatter object using one of the factory methods. + *
    2. Format a value by passing it as an argument to the + * {@link BtcFormat#format(Object)} method. + *
    3. Parse a value by passing a String-type + * representation of it to the {@link BtcFormat#parse(String)} method.
    + * + *

    For example, depending on your locale, values might be formatted + * and parsed as follows: + * + *

    + * BtcFormat f = BtcFormat.getInstance();
    + * String c = f.format(Coin.COIN);                // "BTC 1.00"
    + * String k = f.format(Coin.COIN.multiply(1000)); // "BTC 1,000.00"
    + * String m = f.format(Coin.COIN.divide(1000));   // "mBTC 1.00"
    + * Coin all = f.parseObject("M฿ 21");             // All the money in the world
    + * 
    + * + *

    Auto-Denomination versus Fixed-Denomination

    + * + * There are two provided concrete classes, one that automatically denominates values to + * be formatted, {@link BtcAutoFormat}, and another that formats any value in units of a + * fixed, specified denomination, {@link BtcFixedFormat}. + * + *
    Automatic Denomination
    + * + * Automatic denomination means that the formatter adjusts the denominational units in which a + * formatted number is expressed based on the monetary value that number represents. An + * auto-denominating formatter is defined by its style, specified by one of the enumerated + * values of {@link BtcAutoFormat.Style}. There are two styles constants: {@link + * BtcAutoFormat.Style#CODE} (the default), and {@link BtcAutoFormat.Style#SYMBOL}. The + * difference is that the CODE style uses an internationally-distinct currency + * code, such as "BTC", to indicate the units of denomination, while the + * SYMBOL style uses a possibly-ambiguous currency symbol such as + * "฿". + * + *

    The denomination used when formatting will be either bitcoin, millicoin + * or microcoin, depending on the value being represented, chosen so as to minimize the number + * of consecutive zeros displayed without losing precision. For example, depending on the + * locale, a value of one bitcoin might be formatted as

    ฿1.00
    where a value + * exceeding that by one satoshi would be
    µ฿1,000,000.01
    + * + *
    Fixed Denomination
    + * + * Fixed denomination means that the same denomination of units is used for every value that is + * formatted or parsed by a given formatter instance. A fixed-denomination formatter is + * defined by its scale, which is the number of places one must shift the decimal point in + * increasing precision to convert the representation of a given quantity of bitcoins into a + * representation of the same value denominated in the formatter's units. For example, a scale + * value of 3 specifies a denomination of millibitcoins, because to represent + * 1.0000 BTC, or one bitcoin, in millibitcoins, one shifts the decimal point + * three places, that is, to 1000.0 mBTC. + * + *

    Construction

    + * + * There are two ways to obtain an instance of this class:
      + *
    1. Use one of the factory methods; or + *
    2. Use a {@link BtcFormat.Builder} object.
    + * + *

    The factory methods are appropriate for basic use where the default + * configuration is either used or modified. The Builder + * class provides more control over the configuration, and gives + * access to some features not available through the factory methods, + * such as using custom formatting patterns and currency symbols. + * + *

    Factory Methods
    + * + * Although formatting and parsing is performed by one of the concrete + * subclasses, you can obtain formatters using the various static factory + * methods of this abstract base class BtcFormat. There + * are a variety of overloaded methods that allow you to obtain a + * formatter that behaves according to your needs. + * + *

    The primary distinction is between automatic- and + * fixed-denomination formatters. By default, the + * getInstance() method with no arguments returns a new, + * automatic-denominating BtcAutoFormat instance for your + * default locale that will display exactly two fractional decimal + * places and a currency code. For example, if you happen to be in + * the USA: + * + *

    + * BtcFormat f = BtcFormat.getInstance();
    + * String s = f.format(Coin.COIN); // "BTC 1.00"
    + * 
    + * + *

    The first argument to getInstance() can determine + * whether you get an auto- or fixed-denominating formatter. If the + * type of the first argument is an int, then the value + * of that int will be interpreted as the decimal-place scale of + * the {@link BtcFixedFormat} instance that is returned, and thus will + * determine its denomination. For example, if you want to format + * values in units of microbitcoins: + * + *

    BtcFormat m = BtcFormat.getInstance(6);
    + *String s = m.format(Coin.COIN); // "1,000,000.00"
    + * 
    + * + *

    This class provides several constants bound to common scale values: + * + *

    BtcFormat milliFormat = BtcFormat.getInstance(MILLICOIN_SCALE);
    + * + *

    Alternatively, if the type of the first argument to + * getInstance() is one of the enumerated values of the + * {@link BtcAutoFormat.Style} type, either CODE or + * SYMBOL, then you will get a {@link BtcAutoFormat} + * instance that uses either a currency code or symbol, respectively, + * to indicate the results of its auto-denomination. + * + *

    + * BtcFormat s = BtcFormat.getInstance(SYMBOL);
    + * Coin value = Coin.parseCoin("0.1234");
    + * String mil = s.format(value);              // "₥฿123.40"
    + * String mic = s.format(value.divide(1000)); // "µ฿123.40"
    + * 
    + * + *

    An alternative way to specify whether you want an auto- or fixed-denomination formatter + * is to use one of the factory methods that is named to indicate that characteristics of the + * new instance returned. For fixed-denomination formatters, these methods are {@link + * #getCoinInstance()}, {@link #getMilliInstance()}, and {@link #getMicroInstance()}. These + * three methods are equivalent to invoking getInstance() with a first argument of + * 0, 3 and 6, respectively. For auto-denominating + * formatters the relevant factory methods are {@link #getCodeInstance()} and {@link + * #getSymbolInstance()}, which are equivalent to getInstance(Style.CODE), and + * getInstance(Style.SYMBOL). + * + *

    Regardless of how you specify whether your new formatter is to be of automatic- or + * fixed-denomination, the next (and possibly first) parameter to each of the factory methods + * is an optional Locale value. + * + * For example, here we construct four instances for the same locale that each format + * differently the same one-bitcoin value: + * + *

    + * // Next line returns "1,00 BTC"
    + * BtcFormat.getInstance(Locale.GERMANY).format(Coin.COIN);
    + * // Next line returns "1,00 ฿"
    + * BtcFormat.getInstance(SYMBOL, Locale.GERMANY).format(Coin.COIN);
    + * // Next line returns "1.000,00"
    + * BtcFormat.getMilliInstance(Locale.GERMANY).format(Coin.COIN);
    + * // Next line returns "10.000,00"
    + * BtcFormat.getInstance(4, Locale.GERMANY).format(Coin.COIN);
    + * 
    + * + * Omitting such a Locale parameter will give you a + * formatter for your default locale. + * + *

    The final (and possibly only) arguments to the factory methods serve to set the default + * number of fractional decimal places that will be displayed when formatting monetary values. + * In the case of an auto-denominating formatter, this can be a single int value, + * which will determine the number of fractional decimal places to be used in all cases, except + * where either (1) doing so would provide a place for fractional satoshis, or (2) that default + * value is overridden when invoking the format() method as described below. + * + *

    In the case of a fixed-denomination formatter, you can pass any number of + * int values. The first will determine the minimum number of fractional decimal + * places, and each following int value specifies the size of an optional group of + * decimal-places to be displayed only if useful for expressing precision. As with auto-denominating + * formatters, numbers will never be formatted with a decimal place that represents a + * fractional quantity of satoshis, and these defaults can be overridden by arguments to the + * format() method. See below for examples. + * + *

    The Builder Class
    + * + * A new {@link BtcFormat.Builder} instance is returned by the {@link #builder()} method. Such + * an object has methods that set the configuration parameters of a BtcFormat + * object. Its {@link Builder#build()} method constructs and returns a BtcFormat instance + * configured according to those settings. + * + *

    In addition to setter-methods that correspond to the factory-method parameters explained + * above, a Builder also allows you to specify custom formatting and parsing + * patterns and currency symbols and codes. For example, rather than using the default + * currency symbol, which has the same unicode character point as the national currency symbol of + * Thailand, some people prefer to use a capital letter "B" with a vertical overstrike. + * + *

    + * BtcFormat.Builder builder = BtcFormat.builder();
    + * builder.style(SYMBOL);
    + * builder.symbol("B\u20e6"); // unicode char "double vertical stroke overlay"
    + * BtcFormat f = builder.build();
    + * String out = f.format(COIN); // "B⃦1.00" depending on locale
    + * 
    + * + * The Builder methods are chainable. So, for example, if you are + * deferential to ISO 4217, you might construct a formatter in a single line this way: + * + *
    + * BtcFormat f = BtcFormat.builder().style(CODE).code("XBT").build();
    + * String out = f.format(COIN); // "XBT 1.00"
    + * 
    + * + *

    See the documentation of the {@link BtcFormat.Builder} class for details. + * + *

    Formatting

    + * + *

    You format a Bitcoin monetary value by passing it to the {@link BtcFormat#format(Object)} + * method. This argument can be either a {@link com.google.bitcoin.core.Coin}-type object or a + * numerical object such as {@link java.lang.Long} or {@link java.math.BigDecimal}. + * Integer-based types such as {@link java.math.BigInteger} are interpreted as representing a + * number of satoshis, while a {@link java.math.BigDecimal} is interpreted as representing a + * number of bitcoins. A value having a fractional amount of satoshis is rounded to the + * nearest whole satoshi at least, and possibly to a greater unit depending on the number of + * fractional decimal-places displayed. The format() method will not accept an + * argument whose type is String, Float nor Double. + * + *

    Subsequent to the monetary value to be formatted, the {@link #format(Object)} method also + * accepts as arguments optional int values that specify the number of decimal + * places to use to represent the fractional portion of the number. This overrides the + * default, and enables a single formatter instance to be reused, formatting different values + * that require different numbers of fractional decimal places. These parameters have the same + * meaning as those that set the default values in the factory methods as described above. + * Namely, a single int value determines the minimum number of fractional decimal + * places that will be used in all cases, to a precision limit of satoshis. Instances of + * {@link BtcFixedFormat} also accept a variable-length sequence of additional int + * values, each of which specifies the size of a group of fractional decimal-places to be used + * in addition to all preceding places, only if useful to express precision, and only to a + * maximum precision of satoshis. For example: + * + *

    + * BtcFormat f = BtcFormat.getCoinInstance();
    + * Coin value = COIN.add(Coin.valueOf(5)); // 100000005 satoshis
    + * f.format(value, 2);       // "1.00"
    + * f.format(value, 3);       // "1.000"
    + * f.format(value, 2, 3);    // "1.00" three more zeros doesn't help 
    + * f.format(value, 2, 3, 3); // "1.00000005" 
    + * f.format(value, 2, 3, 4); // "1.00000005" fractions of satoshis have no place
    + * f.format(value, 2, 3, 2); // "1.0000001" rounds to nearest usable place
    + * 
    + * + *

    Note that if using all the fractional decimal places in a specified group would give a + * place to fractions of satoshis, then the size of that group will be reduced to a maximum + * precision of satoshis. Either all or none of the allowed decimal places of that group will + * still be applied as doing so is useful for expressing the precision of the value being + * formatted. + * + *

    Several convenient constants of repeating group-size sequences are provided: + * {@link BtcFixedFormat#REPEATING_PLACES}, {@link + * BtcFixedFormat#REPEATING_DOUBLETS} and {@link + * BtcFixedFormat#REPEATING_TRIPLETS}. These signify repeating groups + * of one, two and three decimals places, respectively. For example, + * to display only as many fractional places as useful in order to + * prevent hanging zeros on the least-significant end of formatted + * numbers: + * + *

    + * format(value, 0, REPEATING_PLACES);
    + * 
    + * + *

    When using an automatically-denominating formatter, you might + * want to know what denomination was chosen. You can get the + * currency-units indicator, as well as any other field in the + * formatted output, by using a {@link java.text.FieldPosition} instance + * constructed using an appropriate constant from the {@link + * java.text.NumberFormat.Field} class: + * + *

    + * BtcFormat de = BtcFormat.getInstance(Locale.GERMANY);
    + * FieldPosition currField = new FieldPosition(NumberFormat.Field.CURRENCY);
    + * // next line formats the value as "987.654.321,23 µBTC"
    + * String output = de.format(valueOf(98765432123L), new StringBuffer(), currField);
    + * // next line sets variable currencyCode to "µBTC"
    + * String currencyCode = output.substring(currField.getBeginIndex(), currField.getEndIndex()));
    + * 
    + * + *

    When using a fixed-denomination formatter whose scale can be expressed as a standard + * "metric" prefix, you can invoke the code() and symbol() methods to + * obtain a String whose value is the appropriate currency code or symbol, + * respectively, for that formatter. + * + *

    + * BtcFixedFormat kilo = (BtcFixedFormat)BtcFormat(-3); // scale -3 for kilocoins
    + * Coin value = Coin.parseCoin("1230");
    + * // variable coded will be set to "kBTC 1.23"
    + * String coded = kilo.code() + " " + kilo.format(value);
    + * // variable symbolic will be set to "k฿1.23"
    + * String symbolic = kilo.symbol() + kilo.format(value);
    + * BtcFormat(4).code(); // unnamed denomination has no code; raises exception
    + * 
    + * + *
    Formatting for Tabular Columns
    + * + * When displaying tables of monetary values, you can lessen the + * risk of human misreading-error by vertically aligning the decimal + * separator of those values. This example demonstrates one way to do that: + * + *
    + * // The elements of this array are the values we will format:
    + * Coin[] rows = {MAX_MONEY, MAX_MONEY.subtract(SATOSHI), Coin.parseCoin("1234"),
    + *                COIN, COIN.divide(1000),
    + *                valueOf(10000), valueOf(1000), valueOf(100),
    + *                SATOSHI};
    + * BtcFormat f = BtcFormat.getCoinInstance(2, REPEATING_PLACES);
    + * FieldPosition fp = new FieldPosition(DECIMAL_SEPARATOR); // see java.text.NumberFormat.Field
    + * String[] output = new String[rows.length];
    + * int[] indexes = new int[rows.length];
    + * int maxIndex = 0;
    + * for (int i = 0; i < rows.length; i++) {
    + *     output[i] = f.format(rows[i], new StringBuffer(), fp).toString();
    + *     indexes[i] = fp.getBeginIndex();
    + *     if (indexes[i] > maxIndex) maxIndex = indexes[i];
    + * }
    + * for (int i = 0; i < output.length; i++) {
    + *     System.out.println(repeat(" ", (maxIndex - indexes[i])) + output[i]);
    + * }
    + * 
    + * + * Assuming you are using a monospaced font, and depending on your + * locale, the foregoing will print the following: + * + *
    + * 21,000,000.00
    + * 20,999,999.99999999
    + *      1,234.00
    + *          1.00
    + *          0.001
    + *          0.0001
    + *          0.00001
    + *          0.000001
    + *          0.00000001
    + * 
    + * + * If you need to vertically-align columns printed in a proportional font, + * then see the documentation for the {@link java.text.NumberFormat} class + * for an explanation of how to do that. + * + *

    Parsing

    + * + *

    The {@link #parse(String)} method accepts a String argument, and returns a + * {@link Coin}-type value. The difference in parsing behavior between instances of {@link + * BtcFixedFormat} and {@link BtcAutoFormat} is analogous to the difference in formatting + * behavior between instances of those classes. Instances of {@link BtcAutoFormat} recognize + * currency codes and symbols in the String being parsed, and interpret them as + * indicators of the units in which the number being parsed is denominated. On the other hand, + * instances of {@link BtcFixedFormat} by default recognize no codes nor symbols, but rather + * interpret every number as being denominated in the units that were specified when + * constructing the instance doing the parsing. This default behavior of {@link + * BtcFixedFormat} can be overridden by setting a parsing pattern that includes a currency sign + * using the {@link BtcFormat.Builder#pattern()} method. + * + *

    The {@link BtcAutoFormat#parse(String)} method of {@link BtcAutoFormat} (and of + * {@link BtcAutoFormat} configured with applicable non-default pattern) will recognize a + * variety of currency symbols and codes, including all standard international (metric) + * prefixes from micro to mega. For example, denominational units of microcoins may be + * specified by µ฿, u฿, µB⃦, µɃ, + * µBTC or other appropriate permutations of those characters. Additionally, if + * either or both of a custom currency code or symbol is configured using {@link + * BtcFormat.Builder#code()} or {@link BtcFormat.Builder.code()}, then such code or symbol will + * be recognized in addition to those recognized by default.. + * + *

    Instances of this class that recognize currency signs will recognize both currency + * symbols and codes, regardless of which that instance uses for formatting. However, if the + * style is CODE (and unless overridden by a custom pattern) then a space character must + * separate the units indicator from the number. When parsing with a SYMBOL-style + * BtcFormat instance, on the other hand, whether or not the units indicator must + * be separated by a space from the number is determined by the locale. The {@link + * BtcFormat#pattern()} method returns a representation of the pattern that + * can be examined to determine whether a space must separate currency signs from numbers in + * parsed Strings. + * + *

    When parsing, if the currency-units indicator is absent, then a {@link BtcAutoFormat} + * instance will infer a denomination of bitcoins while a {@link BtcFixedFormat} will infer the + * denomination in which it expresses formatted values. Note: by default (unless overridden by + * a custom pattern), if the locale or style requires a space to separate the number from the + * units indicator, that space must be present in the String to be parsed, even if the units + * indicator is absent. + * + *

    The parse() method returns an instance of the + * {@link Coin} class. Therefore, attempting to parse a value greater + * than the maximum that a Coin object can represent will + * raise a ParseException, as will any other detected + * parsing error. + * + *

    Limitations

    + * + *
    Parsing
    + * + * Parsing is performed by an underlying {@link java.text.NumberFormat} object. While this + * delivers the benefit of recognizing locale-specific patterns, some have criticized other + * aspects of its behavior. For example, see this article by Joe Sam + * Shirah. In particular, explicit positive-signs are not recognized. If you are parsing + * input from end-users, then you should consider whether you would benefit from any of the + * work-arounds mentioned in that article. + * + *
    Exotic Locales
    + * + * This class is not well-tested in locales that use non-ascii + * character sets, especially those where writing proceeds from + * right-to-left. Helpful feedback in that regard is appreciated. + * + *

    Thread-Safety

    + * + *

    Instances of this class are immutable. + * + * @see java.text.Format + * @see java.text.NumberFormat + * @see java.text.DecimalFormat + * @see java.text.DecimalFormatSymbols + * @see java.text.FieldPosition + * @see com.google.bitcoin.core.Coin + */ + +public abstract class BtcFormat extends Format { + + /* CONCURRENCY NOTES + * + * There is one mutable member of this class, the `DecimalFormat` object bound to variable + * `numberFormat`. The relevant methods invoked on it are: setMinimumFractionDigits(), + * setMaximumFractionDigits(), and setDecimalFormatSymbols(), along with the respective + * getter methods corresponding to each. The first two methods are used to set the number + * of fractional decimal places displayed when formatting, which is reflected in the + * patterns returned by the public pattern() and localizedPattern() methods. The last + * method sets the value of that object's member `DecimalFormatSymbols` object for + * formatting and parsing, which is also reflected in the aforementioned patterns. The + * patterns, which are the passed-through return values of the DecimalFormat object's + * toPattern() and toLocalizedPattern() methods, and the value of the DecimalFormat + * object's DecimalFormatSymbols member are among the values compared between instances of + * this class in determining the return values of the `equals()` and `hashCode()` methods. + * + * From the foregoing, you can understand that immutability is achieved as follows: access + * to the variable `numberFormat` referent's fraction-digits and format-symbols fields are + * synchronized on that DecimalFormat object. The state of those fraction-digits limits + * and decimal-format symbols must be returned to a static state after being changed for + * formatting or parsing since the user can see them reflected in the return values of + * above-mentioned methods and because `equals()` and `hashCode()` use them for + * comparisons. + */ + + /** The conventional international currency code for bitcoins: "BTC" */ + private static final String COIN_CODE = "BTC"; + /** The default currency symbols for bitcoins */ + private static final String COIN_SYMBOL = "฿"; + /** An alternative currency symbol to use in locales where the default symbol is used for the national currency. */ + protected static final String COIN_SYMBOL_ALT = "Ƀ"; + + protected final DecimalFormat numberFormat; // warning: mutable + protected final int minimumFractionDigits; + protected final List decimalGroups; + + /* Scale is the number of decimal-places difference from same value in bitcoins */ + /** A constant useful for specifying a denomination of bitcoins, the int value + * 0. */ + public static final int COIN_SCALE = 0; + + /** A constant useful for specifying a denomination of millibitcoins, the int + * value 3. */ + public static final int MILLICOIN_SCALE = 3; + + /** A constant useful for specifying a denomination of microbitcoins, the int + * value 6. */ + public static final int MICROCOIN_SCALE = 6; + + /** Return the number of decimal places by which any value denominated in the + * units indicated by the given scale differs from that same value denominated in satoshis */ + private static int offSatoshis(int scale) { return Coin.SMALLEST_UNIT_EXPONENT - scale; } + + private static Locale defaultLocale() { return Locale.getDefault(Locale.Category.FORMAT); } + + /** + *

    This class constructs new instances of {@link BtcFormat}, allowing for the + * configuration of those instances before they are constructed. After obtaining a + * Builder object from the {@link BtcFormat#builder()} method, invoke the + * necessary setter methods to obtain your desired configuration. Finaly, the {@link + * #build()} method returns a new BtcFormat object that has the specified + * configuration. + * + *

    All the setter methods override defaults. Invoking build() without invoking any + * of the setting methods is equivalent to invoking {@link BtcFormat#getInstance()} with no arguments. + * + *

    Each setter methods returns the same instance on which it is invoked, + * thus these methods can be chained. + * + *

    Instances of this class are not thread-safe. + */ + public static class Builder { + + private enum Variant { + AUTO { @Override BtcFormat newInstance(Builder b) { + return getInstance(b.style, b.locale, b.minimumFractionDigits); + }}, + FIXED, + UNSET; + BtcFormat newInstance(Builder b) { + return getInstance(b.scale, b.locale, b.minimumFractionDigits, b.fractionGroups); + } + } + // Parameters are initialized to default or unset values + private Variant variant = Variant.UNSET; + private Locale locale = defaultLocale(); + private int minimumFractionDigits = 2; + private int[] fractionGroups = {}; + private Style style = BtcAutoFormat.Style.CODE; + private int scale = 0; + private String symbol = "",code = "",pattern = "",localizedPattern = ""; + + private Builder() {} + + /** Specify the new BtcFormat is to be automatically-denominating. + * The argument determines which of either codes or symbols the new BtcFormat + * will use by default to indicate the denominations it chooses when formatting values. + * + *

    Note that the Style argument specifies the + * default style, which is overridden by invoking + * either {@link #pattern(String)} or {@link #localizedPattern(String)}. + * + * @throws IllegalArgumentException if {@link #scale(int)} has + * previously been invoked on this instance.*/ + public Builder style(BtcAutoFormat.Style val) { + if (variant == Variant.FIXED) + throw new IllegalStateException("You cannot invoke both style() and scale()"); + variant = Variant.AUTO; + style = val; + return this; + } + + /** Specify the number of decimal places in the fraction part of formatted numbers. + * This is equivalent to the {@link #minimumFractionDigits(int)} method, but named + * appropriately for the context of generating {@link BtcAutoFormat} instances. + * + *

    If neither this method nor minimumFactionDigits() is invoked, the default value + * will be 2. */ + public Builder fractionDigits(int val) { return minimumFractionDigits(val); } + + /** Specify a fixed-denomination of units to use when formatting and parsing values. + * The argument specifies the number of decimal places, in increasing + * precision, by which each formatted value will differ from that same value + * denominated in bitcoins. For example, a denomination of millibitcoins is specified + * with a value of 3. + * + *

    The BtcFormat class provides appropriately named + * int-type constants for the three common values, {@link BtcFormat#COIN_SCALE}, + * {@link BtcFormat#MILLICOIN_SCALE} {@link BtcFormat#MICROCOIN_SCALE}. + * + *

    If neither this method nor {@link #style(BtcAutoFormat.Style)} is invoked on a + * Builder, then the BtcFormat will default to a + * fixed-denomination of bitcoins, equivalent to invoking this method with an argument + * of 0. */ + public Builder scale(int val) { + if (variant == Variant.AUTO) + throw new IllegalStateException("You cannot invoke both scale() and style()"); + variant = Variant.FIXED; + scale = val; + return this; + } + + /** Specify the minimum number of decimal places in the fraction part of formatted values. + * This method is equivalent to {@link #fractionDigits(int)}, but named appropriately for + * the context of generating a fixed-denomination formatter. + * + *

    If neither this method nor fractionDigits() is invoked, the default value + * will be 2. */ + public Builder minimumFractionDigits(int val) { minimumFractionDigits = val; return this; } + + /** Specify the sizes of a variable number of optional decimal-place groups in the + * fraction part of formatted values. A group of each specified size will be used in + * addition to all previously applied decimal places only if doing so is useful for + * expressing precision. The size of each group is limited to a maximum precision of + * satoshis. + * + *

    If this method is not invoked, then the number of fractional decimal places will + * be limited to the value passed to {@link #minimumFractionDigits}, or 2 + * if that method is not invoked. */ + public Builder fractionGroups(int... val) { fractionGroups = val; return this; } + + /** Specify the {@link java.util.Locale} for formatting and parsing. + * If this method is not invoked, then the runtime default locale will be used. */ + public Builder locale(Locale val) { locale = val; return this; } + + /** Specify a currency symbol to be used in the denomination-unit indicators + * of formatted values. This method only sets the symbol, but does not cause + * it to be used. You must also invoke either style(SYMBOL), or else apply + * a custom pattern that includes a single currency-sign character by invoking either + * {@link #pattern(String)} or {@link #localizedPattern(String)}. + * + *

    Specify only the base symbol. The appropriate prefix will be applied according + * to the denomination of formatted and parsed values. */ + public Builder symbol(String val) { symbol = val; return this; } + + /** Specify a custom currency code to be used in the denomination-unit indicators + * of formatted values. This method only sets the code, but does not cause + * it to be used. You must also invoke either style(CODE), or else apply + * a custom pattern that includes a double currency-sign character by invoking either + * {@link #pattern(String)} or {@link #localizedPattern(String)}. + * + *

    Specify only the base code. The appropriate prefix will be applied according + * to the denomination of formatted and parsed values. */ + public Builder code(String val) { code = val; return this; } + + /** Use the given pattern when formatting and parsing. The format of this pattern is + * identical to that used by the {@link java.text.DecimalFormat} class. + * + *

    If the pattern lacks a negative subpattern, then the formatter will indicate + * negative values by placing a minus sign immediately preceding the number part of + * formatted values. + * + *

    Note that while the pattern format specified by the {@link + * java.text.DecimalFormat} class includes a mechanism for setting the number of + * fractional decimal places, that part of the pattern is ignored. Instead, use the + * {@link #fractionDigits(int)}, {@link #minimumFractionDigits(int)} and {@link + * #fractionGroups(int...)} methods. + * + *

    Warning: if you set a pattern that includes a currency-sign for a + * fixed-denomination formatter that uses a non-standard scale, then an exception will + * be raised when you try to format a value. The standard scales include all for + * which a metric prefix exists from micro to mega. + * + *

    Note that by applying a pattern you override the configured formatting style of + * {@link BtcAutoFormat} instances. */ + public Builder pattern(String val) { + if (localizedPattern != "") + throw new IllegalStateException("You cannot invoke both pattern() and localizedPattern()"); + pattern = val; return this; + } + + /** Use the given localized-pattern for formatting and parsing. The format of this + * pattern is identical to the patterns used by the {@link java.text.DecimalFormat} + * class. + * + *

    The pattern is localized according to the locale of the BtcFormat + * instance, the symbols for which can be examined by inspecting the {@link + * java.text.DecimalFormatSymbols} object returned by {@link BtcFormat#symbols()}. + * So, for example, if you are in Germany, then the non-localized pattern of + *

    "#,##0.###"
    would be localized as
    "#.##0,###"
    + * + *

    If the pattern lacks a negative subpattern, then the formatter will indicate + * negative values by placing a minus sign immediately preceding the number part of + * formatted values. + * + *

    Note that while the pattern format specified by the {@link + * java.text.DecimalFormat} class includes a mechanism for setting the number of + * fractional decimal places, that part of the pattern is ignored. Instead, use the + * {@link #fractionDigits(int)}, {@link #minimumFractionDigits(int)} and {@link + * #fractionGroups(int...)} methods. + * + *

    Warning: if you set a pattern that includes a currency-sign for a + * fixed-denomination formatter that uses a non-standard scale, then an exception will + * be raised when you try to format a value. The standard scales include all for + * which a metric prefix exists from micro to mega. + * + *

    Note that by applying a pattern you override the configured formatting style of + * {@link BtcAutoFormat} instances. */ + public Builder localizedPattern(String val) { + if (pattern != "") + throw new IllegalStateException("You cannot invoke both pattern() and localizedPattern()."); + localizedPattern = val; + return this; + } + + /** Return a new {@link BtcFormat} instance. The object returned will be configured according + * to the state of this Builder instance at the time this method is invoked. */ + public BtcFormat build() { + BtcFormat f = variant.newInstance(this); + if (symbol != "" || code != "") { synchronized(f.numberFormat) { + DecimalFormatSymbols defaultSigns = f.numberFormat.getDecimalFormatSymbols(); + setSymbolAndCode(f.numberFormat, + symbol != "" ? symbol : defaultSigns.getCurrencySymbol(), + code != "" ? code : defaultSigns.getInternationalCurrencySymbol() + ); + }} + if (localizedPattern != "" || pattern != "") { + int places = f.numberFormat.getMinimumFractionDigits(); + if (localizedPattern != "") f.numberFormat.applyLocalizedPattern(negify(localizedPattern)); + else f.numberFormat.applyPattern(negify(pattern)); + f.numberFormat.setMinimumFractionDigits(places); + f.numberFormat.setMaximumFractionDigits(places); + } + return f; + } + + } + + /** Return a new {@link Builder} object. See the documentation of that class for usage details. */ + public static Builder builder() { return new Builder(); } + + /** This single constructor is invoked by the overriding subclass constructors. */ + protected BtcFormat(DecimalFormat numberFormat, int minDecimals, List groups) { + checkArgument(minDecimals >= 0, "There can be no fewer than zero fractional decimal places"); + this.numberFormat = numberFormat; + this.numberFormat.setParseBigDecimal(true); + this.numberFormat.setRoundingMode(HALF_UP); + this.minimumFractionDigits = minDecimals; + this.numberFormat.setMinimumFractionDigits(this.minimumFractionDigits); + this.numberFormat.setMaximumFractionDigits(this.minimumFractionDigits); + this.decimalGroups = groups; + synchronized (this.numberFormat) { setSymbolAndCode( + this.numberFormat, + (this.numberFormat.getDecimalFormatSymbols().getCurrencySymbol().contains(COIN_SYMBOL)) + ? COIN_SYMBOL_ALT + : COIN_SYMBOL, + COIN_CODE + );} + } + + /** + * Return a new instance of this class using all defaults. The returned formatter will + * auto-denominate values so as to minimize zeros without loss of precision and display a + * currency code, for example "BTC", to indicate that denomination. The + * returned object will uses the default locale for formatting the number and placement of + * the currency-code. Two fractional decimal places will be displayed in all formatted numbers. + */ + public static BtcFormat getInstance() { return getInstance(defaultLocale()); } + + /** + * Return a new auto-denominating instance that will indicate units using a currency + * symbol, for example, "฿". Formatting and parsing will be done + * according to the default locale. + */ + public static BtcFormat getSymbolInstance() { return getSymbolInstance(defaultLocale()); } + + /** + * Return a new auto-denominating instance that will indicate units using a currency + * code, for example, "BTC". Formatting and parsing will be done + * according to the default locale. + */ + public static BtcFormat getCodeInstance() { return getCodeInstance(defaultLocale()); } + + /** + * Return a new symbol-style auto-formatter with the given number of fractional decimal + * places. Denominational units will be indicated using a currency symbol, for example, + * "฿". The returned object will format the fraction-part of numbers using + * the given number of decimal places, or fewer as necessary to avoid giving a place to + * fractional satoshis. Formatting and parsing will be done according to the default + * locale. + */ + public static BtcFormat getSymbolInstance(int fractionPlaces) { + return getSymbolInstance(defaultLocale(), fractionPlaces); + } + + /** + * Return a new code-style auto-formatter with the given number of fractional decimal + * places. Denominational units will be indicated using a currency code, for example, + * "BTC". The returned object will format the fraction-part of numbers using + * the given number of decimal places, or fewer as necessary to avoid giving a place to + * fractional satoshis. Formatting and parsing will be done according to the default + * locale. + */ + public static BtcFormat getCodeInstance(int minDecimals) { + return getCodeInstance(defaultLocale(), minDecimals); + } + + /** + * Return a new code-style auto-formatter for the given locale. The returned object will + * select denominational units based on each value being formatted, and will indicate those + * units using a currency code, for example, "mBTC". + */ + public static BtcFormat getInstance(Locale locale) { return getCodeInstance(locale); } + + /** + * Return a new code-style auto-formatter for the given locale. The returned object will + * select denominational units based on each value being formatted, and will indicate those + * units using a currency code, for example, "mBTC". + */ + public static BtcFormat getCodeInstance(Locale locale) { return getInstance(CODE, locale); } + + /** + * Return a new code-style auto-formatter for the given locale with the given number of + * fraction places. The returned object will select denominational units based on each + * value being formatted, and will indicate those units using a currency code, for example, + * "mBTC". The returned object will format the fraction-part of numbers using + * the given number of decimal places, or fewer as necessary to avoid giving a place to + * fractional satoshis. + */ + public static BtcFormat getInstance(Locale locale, int minDecimals) { + return getCodeInstance(locale, minDecimals); + } + + /** + * Return a new code-style auto-formatter for the given locale with the given number of + * fraction places. The returned object will select denominational units based on each + * value being formatted, and will indicate those units using a currency code, for example, + * "mBTC". The returned object will format the fraction-part of numbers using + * the given number of decimal places, or fewer as necessary to avoid giving a place to + * fractional satoshis. + */ + public static BtcFormat getCodeInstance(Locale locale, int minDecimals) { + return getInstance(CODE, locale, minDecimals); + } + + /** + * Return a new symbol-style auto-formatter for the given locale. The returned object will + * select denominational units based on each value being formatted, and will indicate those + * units using a currency symbol, for example, "µ฿". + */ + public static BtcFormat getSymbolInstance(Locale locale) { + return getInstance(SYMBOL, locale); + } + + /** + * Return a new symbol-style auto-formatter for the given locale with the given number of + * fraction places. The returned object will select denominational units based on each + * value being formatted, and will indicate those units using a currency symbol, for example, + * "µ฿". The returned object will format the fraction-part of numbers using + * the given number of decimal places, or fewer as necessary to avoid giving a place to + * fractional satoshis. + */ + public static BtcFormat getSymbolInstance(Locale locale, int fractionPlaces) { + return getInstance(SYMBOL, locale, fractionPlaces); + } + + /** + * Return a new auto-denominating formatter. The returned object will indicate the + * denominational units of formatted values using either a currency symbol, such as, + * "฿", or code, such as "mBTC", depending on the value of + * the argument. Formatting and parsing will be done according to the default locale. + */ + public static BtcFormat getInstance(Style style) { return getInstance(style, defaultLocale()); } + + /** + * Return a new auto-denominating formatter with the given number of fractional decimal + * places. The returned object will indicate the denominational units of formatted values + * using either a currency symbol, such as, "฿", or code, such as + * "mBTC", depending on the value of the first argument. The returned object + * will format the fraction-part of numbers using the given number of decimal places, or + * fewer as necessary to avoid giving a place to fractional satoshis. Formatting and + * parsing will be done according to the default locale. + */ + public static BtcFormat getInstance(Style style, int fractionPlaces) { + return getInstance(style, defaultLocale(), fractionPlaces); + } + + /** + * Return a new auto-formatter with the given style for the given locale. + * The returned object that will auto-denominate each formatted value, and + * will indicate that denomination using either a currency code, such as + * "BTC", or symbol, such as "฿", depending on the value + * of the first argument. + *

    The number of fractional decimal places in formatted number will be two, or fewer + * as necessary to avoid giving a place to fractional satoshis. + */ + public static BtcFormat getInstance(Style style, Locale locale) { + return getInstance(style, locale, 2); + } + + /** + * Return a new auto-formatter for the given locale with the given number of fraction places. + * The returned object will automatically-denominate each formatted + * value, and will indicate that denomination using either a currency code, + * such as "mBTC", or symbol, such as "฿", + * according to the given style argument. It will format each number + * according to the given locale. + * + *

    The third parameter is the number of fractional decimal places to use for each + * formatted number, reduced as neccesary when formatting to avoid giving a place to + * fractional satoshis. + */ + public static BtcFormat getInstance(Style style, Locale locale, int fractionPlaces) { + return new BtcAutoFormat(locale, style, fractionPlaces); + } + + /** + * Return a new coin-denominated formatter. The returned object will format and parse + * values according to the default locale, and will format numbers with two fractional + * decimal places, rounding values as necessary. + */ + public static BtcFormat getCoinInstance() { return getCoinInstance(defaultLocale()); } + + private static List boxAsList(int[] elements) throws IllegalArgumentException { + List list = new ArrayList(elements.length); + for (int e : elements) { + checkArgument(e > 0, "Size of decimal group must be at least one."); + list.add(e); + }; + return list; + } + + /** + * Return a new coin-denominated formatter with the specified fraction-places. The + * returned object will format and parse values according to the default locale, and will + * format the fraction part of numbers with at least two decimal places. The sizes of + * additional groups of decimal places can be specified by a variable number of + * int arguments. Each optional decimal-place group will be applied only if + * useful for expressing precision, and will be only partially applied if necessary to + * avoid giving a place to fractional satoshis. + */ + public static BtcFormat getCoinInstance(int minFractionPlaces, int... groups) { + return getInstance(COIN_SCALE, defaultLocale(), minFractionPlaces, boxAsList(groups)); + } + + /** + * Return a new coin-denominated formatter for the given locale. The returned object will + * format the fractional part of numbers with two decimal places, rounding as necessary. + */ + public static BtcFormat getCoinInstance(Locale locale) { + return getInstance(COIN_SCALE, locale, 2); + } + + /** + * Return a newly-constructed instance for the given locale that will format + * values in terms of bitcoins, with the given minimum number of fractional + * decimal places. Optionally, repeating integer arguments can be passed, each + * indicating the size of an additional group of fractional decimal places to be + * used as necessary to avoid rounding, to a limiting precision of satoshis. + */ + public static BtcFormat getCoinInstance(Locale locale, int scale, int... groups) { + return getInstance(COIN_SCALE, locale, scale, boxAsList(groups)); + } + + /** + * Return a new millicoin-denominated formatter. The returned object will format and + * parse values for the default locale, and will format the fractional part of numbers with + * two decimal places, rounding as necessary. + */ + public static BtcFormat getMilliInstance() { return getMilliInstance(defaultLocale()); } + + /** + * Return a new millicoin-denominated formatter for the given locale. The returned object + * will format the fractional part of numbers with two decimal places, rounding as + * necessary. + */ + public static BtcFormat getMilliInstance(Locale locale) { + return getInstance(MILLICOIN_SCALE, locale, 2); + } + + /** + * Return a new millicoin-denominated formatter with the specified fractional decimal + * placing. The returned object will format and parse values according to the default + * locale, and will format the fractional part of numbers with the given minimum number of + * fractional decimal places. Optionally, repeating integer arguments can be passed, each + * indicating the size of an additional group of fractional decimal places to be used as + * necessary to avoid rounding, to a limiting precision of satoshis. + */ + public static BtcFormat getMilliInstance(int scale, int... groups) { + return getInstance(MILLICOIN_SCALE, defaultLocale(), scale, boxAsList(groups)); + } + + /** + * Return a new millicoin-denominated formatter for the given locale with the specified + * fractional decimal placing. The returned object will format the fractional part of + * numbers with the given minimum number of fractional decimal places. Optionally, + * repeating integer arguments can be passed, each indicating the size of an additional + * group of fractional decimal places to be used as necessary to avoid rounding, to a + * limiting precision of satoshis. + */ + public static BtcFormat getMilliInstance(Locale locale, int scale, int... groups) { + return getInstance(MILLICOIN_SCALE, locale, scale, boxAsList(groups)); + } + + /** + * Return a new microcoin-denominated formatter for the default locale. The returned object + * will format the fractional part of numbers with two decimal places, rounding as + * necessary. + */ + public static BtcFormat getMicroInstance() { return getMicroInstance(defaultLocale()); } + + /** + * Return a new microcoin-denominated formatter for the given locale. The returned object + * will format the fractional part of numbers with two decimal places, rounding as + * necessary. + */ + public static BtcFormat getMicroInstance(Locale locale) { + return getInstance(MICROCOIN_SCALE, locale); + } + + /** + * Return a new microcoin-denominated formatter with the specified fractional decimal + * placing. The returned object will format and parse values according to the default + * locale, and will format the fractional part of numbers with the given minimum number of + * fractional decimal places. Optionally, repeating integer arguments can be passed, each + * indicating the size of an additional group of fractional decimal places to be used as + * necessary to avoid rounding, to a limiting precision of satoshis. + */ + public static BtcFormat getMicroInstance(int scale, int... groups) { + return getInstance(MICROCOIN_SCALE, defaultLocale(), scale, boxAsList(groups)); + } + + /** + * Return a new microcoin-denominated formatter for the given locale with the specified + * fractional decimal placing. The returned object will format the fractional part of + * numbers with the given minimum number of fractional decimal places. Optionally, + * repeating integer arguments can be passed, each indicating the size of an additional + * group of fractional decimal places to be used as necessary to avoid rounding, to a + * limiting precision of satoshis. + */ + public static BtcFormat getMicroInstance(Locale locale, int scale, int... groups) { + return getInstance(MICROCOIN_SCALE, locale, scale, boxAsList(groups)); + } + + /** + * Return a new fixeed-denomination formatter with the specified fractional decimal + * placing. The first argument specifies the denomination as the size of the + * shift from coin-denomination in increasingly-precise decimal places. The returned object will format + * and parse values according to the default locale, and will format the fractional part of + * numbers with the given minimum number of fractional decimal places. Optionally, + * repeating integer arguments can be passed, each indicating the size of an additional + * group of fractional decimal places to be used as necessary to avoid rounding, to a + * limiting precision of satoshis. + */ + public static BtcFormat getInstance(int scale, int minDecimals, int... groups) { + return getInstance(scale, defaultLocale(), minDecimals, boxAsList(groups)); + } + + /** + * Return a new fixeed-denomination formatter. The argument specifies the denomination as + * the size of the shift from coin-denomination in increasingly-precise decimal places. + * The returned object will format and parse values according to the default locale, and + * will format the fractional part of numbers with two decimal places, or fewer as + * necessary to avoid giving a place to fractional satoshis. + */ + public static BtcFormat getInstance(int scale) { + return getInstance(scale, defaultLocale()); + } + + /** + * Return a new fixeed-denomination formatter for the given locale. The first argument + * specifies the denomination as the size of the shift from coin-denomination in + * increasingly-precise decimal places. The returned object will format and parse values + * according to the locale specified by the second argument, and will format the fractional + * part of numbers with two decimal places, or fewer as necessary to avoid giving a place + * to fractional satoshis. + */ + public static BtcFormat getInstance(int scale, Locale locale) { + return getInstance(scale, locale, 2); + } + + /** + * Return a new fixed-denomination formatter for the given locale, with the specified + * fractional decimal placing. The first argument specifies the denomination as the size + * of the shift from coin-denomination in increasingly-precise decimal places. The third + * parameter is the minimum number of fractional decimal places to use, followed by + * optional repeating integer parameters each specifying the size of an additional group of + * fractional decimal places to use as necessary to avoid rounding, down to a maximum + * precision of satoshis. + */ + public static BtcFormat getInstance(int scale, Locale locale, int minDecimals, int... groups) { + return getInstance(scale, locale, minDecimals, boxAsList(groups)); + } + + /** + * Return a new fixed-denomination formatter for the given locale, with the specified + * fractional decimal placing. The first argument specifies the denomination as the size + * of the shift from coin-denomination in increasingly-precise decimal places. The third + * parameter is the minimum number of fractional decimal places to use. The third argument + * specifies the minimum number of fractional decimal places in formatted numbers. The + * last argument is a List of Integer values, each of which + * specifies the size of an additional group of fractional decimal places to use as + * necessary to avoid rounding, down to a maximum precision of satoshis. + */ + public static BtcFormat getInstance(int scale, Locale locale, int minDecimals, List groups) { + return new BtcFixedFormat(locale, scale, minDecimals, groups); + } + + /***********************/ + /****** FORMATTING *****/ + /***********************/ + + /** + * Formats a bitcoin monetary value and returns an {@link java.text.AttributedCharacterIterator}. + * By iterating, you can examine what fields apply to each character. This can be useful + * since a character may be part of more than one field, for example a grouping separator + * that is also part of the integer field. + * + * @see java.text.AttributedCharacterIterator + */ + @Override + public AttributedCharacterIterator formatToCharacterIterator(Object obj) { synchronized(numberFormat) { + DecimalFormatSymbols anteSigns = numberFormat.getDecimalFormatSymbols(); + BigDecimal units = denominateAndRound(inSatoshis(obj), minimumFractionDigits, decimalGroups); + List anteDigits = setFormatterDigits(numberFormat, units.scale(), units.scale()); + AttributedCharacterIterator i = numberFormat.formatToCharacterIterator(units); + numberFormat.setDecimalFormatSymbols(anteSigns); + setFormatterDigits(numberFormat, anteDigits.get(0), anteDigits.get(1)); + return i; + }} + + /** + * Formats a bitcoin value as a number and possibly a units indicator and appends the + * resulting text to the given string buffer. The type of monetary value argument can be + * any one of any of the following classes: {@link Coin}, + * Integer, Long, BigInteger, + * BigDecimal. Numeric types that can represent only an integer are interpreted + * as that number of satoshis. The value of a BigDecimal is interpreted as that + * number of bitcoins, rounded to the nearest satoshi as necessary. + * + * @return the StringBuffer passed in as toAppendTo + */ + @Override + public StringBuffer format(Object qty, StringBuffer toAppendTo, FieldPosition pos) { + return format(qty, toAppendTo, pos, minimumFractionDigits, decimalGroups); + } + + /** + * Formats a bitcoin value as a number and possibly a units indicator to a + * String.The type of monetary value argument can be any one of any of the + * following classes: {@link Coin}, Integer, Long, + * BigInteger, BigDecimal. Numeric types that can represent only + * an integer are interpreted as that number of satoshis. The value of a + * BigDecimal is interpreted as that number of bitcoins, rounded to the + * nearest satoshi as necessary. + * + * @param minDecimals The minimum number of decimal places in the fractional part of the formatted number + * @param fractionGroups The sizes of optional additional fractional decimal-place groups + * @throws IllegalArgumentException if the number of fraction places is negative. + */ + public String format(Object qty, int minDecimals, int... fractionGroups) { + return format(qty, new StringBuffer(), new FieldPosition(0), minDecimals, boxAsList(fractionGroups)). + toString(); + } + + /** + * Formats a bitcoin value as a number and possibly a units indicator and appends the + * resulting text to the given string buffer. The type of monetary value argument can be + * any one of any of the following classes: {@link Coin}, + * Integer, Long, BigInteger, + * BigDecimal. Numeric types that can represent only an integer are interpreted + * as that number of satoshis. The value of a BigDecimal is interpreted as that + * number of bitcoins, rounded to the nearest satoshi as necessary. + * + * @param minDecimals The minimum number of decimal places in the fractional part of the formatted number + * @param fractionGroups The sizes of optional additional fractional decimal-place groups + * @throws IllegalArgumentException if the number of fraction places is negative. + */ + public StringBuffer format(Object qty, StringBuffer toAppendTo, FieldPosition pos, + int minDecimals, int... fractionGroups) { + return format(qty, toAppendTo, pos, minDecimals, boxAsList(fractionGroups)); + } + + private StringBuffer format(Object qty, StringBuffer toAppendTo, FieldPosition pos, + int minDecimals, List fractionGroups) { + checkArgument(minDecimals >= 0, "There can be no fewer than zero fractional decimal places"); + synchronized (numberFormat) { + DecimalFormatSymbols anteSigns = numberFormat.getDecimalFormatSymbols(); + BigDecimal denominatedUnitCount = denominateAndRound(inSatoshis(qty), minDecimals, fractionGroups); + List antePlaces = + setFormatterDigits(numberFormat, denominatedUnitCount.scale(), denominatedUnitCount.scale()); + StringBuffer s = numberFormat.format(denominatedUnitCount, toAppendTo, pos); + numberFormat.setDecimalFormatSymbols(anteSigns); + setFormatterDigits(numberFormat, antePlaces.get(0), antePlaces.get(1)); + return s; + } + } + + /** + * Return the denomination for formatting the given value. The returned int + * is the size of the decimal-place shift between the given Bitcoin-value denominated in + * bitcoins and that same value as formatted. A fixed-denomination formatter will ignore + * the arguments. + * + * @param satoshis The number of satoshis having the value for which the shift is calculated + * @param fractionPlaces The number of decimal places available for displaying the + fractional part of the denominated value + * @return The size of the shift in increasingly-precise decimal places + */ + protected abstract int scale(BigInteger satoshis, int fractionPlaces); + + /** Return the denomination of this object. Fixed-denomination formatters will override + * with their configured denomination, auto-formatters with coin denomination. This + * determines the interpretation of parsed numbers lacking a units-indicator. */ + protected abstract int scale(); + + /** + * Takes a bitcoin monetary value that the client wants to format and returns the number of + * denominational units having the equal value, rounded to the appropriate number of + * decimal places. Calls the scale() method of the subclass, which may have the + * side-effect of changing the currency symbol and code of the underlying `NumberFormat` + * object, therefore only invoke this from a synchronized method that resets the NumberFormat. + */ + private BigDecimal denominateAndRound(BigInteger satoshis, int minDecimals, List fractionGroups) { + int scale = scale(satoshis, minDecimals); + BigDecimal denominatedUnitCount = new BigDecimal(satoshis).movePointLeft(offSatoshis(scale)); + int places = calculateFractionPlaces(denominatedUnitCount, scale, minDecimals, fractionGroups); + return denominatedUnitCount.setScale(places, HALF_UP); + } + + /** Sets the number of fractional decimal places to be displayed on the given + * NumberFormat object to the value of the given integer. + * @return The minimum and maximum fractional places settings that the + * formatter had before this change, as an ImmutableList. */ + private static ImmutableList setFormatterDigits(DecimalFormat formatter, int min, int max) { + ImmutableList ante = ImmutableList.of( + formatter.getMinimumFractionDigits(), + formatter.getMaximumFractionDigits() + ); + formatter.setMinimumFractionDigits(min); + formatter.setMaximumFractionDigits(max); + return ante; + } + + /** Return the number of fractional decimal places to be displayed when formatting + * the given number of monetory units of the denomination indicated by the given decimal scale value, + * where 0 = coin, 3 = millicoin, and so on. + * + * @param unitCount the number of monetary units to be formatted + * @param scale the denomination of those units as the decimal-place shift from coins + * @param minDecimals the minimum number of fractional decimal places + * @param fractiongroups the sizes of option fractional decimal-place groups + */ + private static int calculateFractionPlaces( + BigDecimal unitCount, int scale, int minDecimals, List fractionGroups) + { + /* Taking into account BOTH the user's preference for decimal-place groups, AND the prohibition + * against displaying a fractional number of satoshis, determine the maximum possible number of + * fractional decimal places. */ + int places = minDecimals; + for (int group : fractionGroups) { places += group; } + int max = Math.min(places, offSatoshis(scale)); + + places = Math.min(minDecimals,max); + for (int group : fractionGroups) { + /* Compare the value formatted using only this many decimal places to the + * same value using as many places as possible. If there's no difference, then + * there's no reason to continue adding more places. */ + if (unitCount.setScale(places, HALF_UP).compareTo(unitCount.setScale(max, HALF_UP)) == 0) break; + places += group; + if (places > max) places = max; + } + return places; + } + + /** + * Takes an object representing a bitcoin quantity of any type the + * client is permitted to pass us, and return a BigInteger representing the + * number of satoshis having the equivalent value. */ + private static BigInteger inSatoshis(Object qty) { + BigInteger satoshis; + // the value might be bitcoins or satoshis + if (qty instanceof Long || qty instanceof Integer) + satoshis = BigInteger.valueOf(((Number)qty).longValue()); + else if (qty instanceof BigInteger) + satoshis = (BigInteger)qty; + else if (qty instanceof BigDecimal) + satoshis = ((BigDecimal)qty).movePointRight(Coin.SMALLEST_UNIT_EXPONENT). + setScale(0,BigDecimal.ROUND_HALF_UP).unscaledValue(); + else if (qty instanceof Coin) + satoshis = BigInteger.valueOf(((Coin)qty).value); + else + throw new IllegalArgumentException("Cannot format a " + qty.getClass().getSimpleName() + + " as a Bicoin value"); + return satoshis; + } + + /********************/ + /****** PARSING *****/ + /********************/ + + /** + * Parse a String representation of a Bitcoin monetary value. Returns a + * {@link com.google.bitcoin.core.Coin} object that represents the parsed value. + * @see java.text.NumberFormat */ + @Override + public final Object parseObject(String source, ParsePosition pos) { return parse(source, pos); } + + private class ScaleMatcher { + public Pattern pattern; + public int scale; + ScaleMatcher(Pattern p, int s) { pattern = p; scale = s; } + } + + /* Lazy initialization; No reason to create all these objects unless needed for parsing */ + // coin indicator regex String; TODO: does this need to be volatile? + private volatile String ci = "(" + COIN_SYMBOL + "|" + COIN_SYMBOL_ALT + "|B⃦|" + COIN_CODE + "|XBT)"; + private Pattern coinPattern; + private volatile ScaleMatcher[] denoms; + ScaleMatcher[] denomMatchers() { + ScaleMatcher[] result = denoms; + if (result == null) { synchronized(this) { + result = denoms; + if (result == null) { + if (! coinSymbol().matches(ci)) ci = ci.replaceFirst("\\(", "(" + coinSymbol() + "|"); + if (! coinCode().matches(ci)) { + ci = ci.replaceFirst("\\)", "|" + coinCode() + ")"); + } + coinPattern = Pattern.compile(ci + "?"); + result = denoms = new ScaleMatcher[]{ + new ScaleMatcher(Pattern.compile("¢" + ci + "?|c" + ci), 2), // centi + new ScaleMatcher(Pattern.compile("₥" + ci + "?|m" + ci), MILLICOIN_SCALE), + new ScaleMatcher(Pattern.compile("([µu]" + ci + ")"), MICROCOIN_SCALE), + new ScaleMatcher(Pattern.compile("(da" + ci + ")"), -1), // deka + new ScaleMatcher(Pattern.compile("(h" + ci + ")"), -2), // hekto + new ScaleMatcher(Pattern.compile("(k" + ci + ")"), -3), // kilo + new ScaleMatcher(Pattern.compile("(M" + ci + ")"), -6) // mega + }; + } + }} + return result; + } + + /** Set both the currency symbol and international code of the underlying {@link + * java.text.NumberFormat} object to the value of the given String. + * This method is invoked in the process of parsing, not formatting. + * + * Only invoke this from code synchronized on the value of the first argument, and don't + * forget to put the symbols back otherwise equals(), hashCode() and immutability will + * break. */ + private static DecimalFormatSymbols setSymbolAndCode(DecimalFormat numberFormat, String sign) { + return setSymbolAndCode(numberFormat, sign, sign); + } + + /** Set the currency symbol and international code of the underlying {@link + * java.text.NumberFormat} object to the values of the last two arguments, respectively. + * This method is invoked in the process of parsing, not formatting. + * + * Only invoke this from code synchronized on value of the first argument, and don't + * forget to put the symbols back otherwise equals(), hashCode() and immutability will + * break. */ + private static DecimalFormatSymbols setSymbolAndCode(DecimalFormat numberFormat, String symbol, String code) { + assert Thread.holdsLock(numberFormat); + DecimalFormatSymbols fs = numberFormat.getDecimalFormatSymbols(); + DecimalFormatSymbols ante = (DecimalFormatSymbols)fs.clone(); + fs.setInternationalCurrencySymbol(code); + fs.setCurrencySymbol(symbol); + numberFormat.setDecimalFormatSymbols(fs); + return ante; + } + + /** + * Set both the currency symbol and code of the underlying, mutable NumberFormat object + * according to the given denominational units scale factor. This is for formatting, not parsing. + * + *

    Set back to zero when you're done formatting otherwise immutability, equals() and + * hashCode() will break! + * + * @param scale Number of places the decimal point will be shifted when formatting + * a quantity of satoshis. + * @return The DecimalFormatSymbols before changing + */ + protected static void prefixUnitsIndicator(DecimalFormat numberFormat, int scale) { + assert Thread.holdsLock(numberFormat); // make sure caller intends to reset before changing + DecimalFormatSymbols fs = (DecimalFormatSymbols)numberFormat.getDecimalFormatSymbols(); + setSymbolAndCode(numberFormat, + prefixSymbol(fs.getCurrencySymbol(), scale), prefixCode(fs.getInternationalCurrencySymbol(), scale) + ); + } + + /** Parse a String representation of a Bitcoin monetary value. If this + * object's pattern includes a currency sign, either symbol or code, as by default is true + * for instances of {@link BtcAutoFormat} and false for instances of {@link + * BtcFixedFormat}, then denominated (i.e., prefixed) currency signs in the parsed String + * will be recognized, and the parsed number will be interpreted as a quantity of units + * having that recognized denomination. + *

    If the pattern includes a currency sign but no currency sign is detected in the parsed + * String, then the number is interpreted as a quatity of bitcoins. + *

    If the pattern contains neither a currency symbol nor sign, then instances of {@link + * BtcAutoFormat} will interpret the parsed number as a quantity of bitcoins, and instances + * of {@link BtcAutoFormat} will interpret the number as a quantity of that instance's + * configured denomination, which can be ascertained by invoking the {@link + * BtcFixedFormat#symbol()} or {@link BtcFixedFormat#code()} method. + * + *

    Consider using the single-argument version of this overloaded method unless you need to + * keep track of the current parse position. + * + * @return a Coin object representing the parsed value + * @see java.text.ParsePosition + */ + public Coin parse(String source, ParsePosition pos) { + DecimalFormatSymbols anteSigns = null; + int parseScale = COIN_SCALE; // default + Coin coin = null; + synchronized (numberFormat) { + if (numberFormat.toPattern().contains("¤")) { + for(ScaleMatcher d : denomMatchers()) { + Matcher matcher = d.pattern.matcher(source); + if (matcher.find()) { + anteSigns = setSymbolAndCode(numberFormat, matcher.group()); + parseScale = d.scale; + break; + } + } + if (parseScale == COIN_SCALE) { + Matcher matcher = coinPattern.matcher(source); + matcher.find(); + anteSigns = setSymbolAndCode(numberFormat, matcher.group()); + } + } else parseScale = scale(); + + Number number = numberFormat.parse(source, pos); + if (number != null) try { + coin = Coin.valueOf( + ((BigDecimal)number).movePointRight(offSatoshis(parseScale)).setScale(0, HALF_UP).longValue() + ); + } catch (IllegalArgumentException e) { + pos.setIndex(0); + } + if (anteSigns != null) numberFormat.setDecimalFormatSymbols(anteSigns); + } + return coin; + } + + /** Parse a String representation of a Bitcoin monetary value. If this + * object's pattern includes a currency sign, either symbol or code, as by default is true + * for instances of {@link BtcAutoFormat} and false for instances of {@link + * BtcFixedFormat}, then denominated (i.e., prefixed) currency signs in the parsed String + * will be recognized, and the parsed number will be interpreted as a quantity of units + * having that recognized denomination. + *

    If the pattern includes a currency sign but no currency sign is detected in the parsed + * String, then the number is interpreted as a quatity of bitcoins. + *

    If the pattern contains neither a currency symbol nor sign, then instances of {@link + * BtcAutoFormat} will interpret the parsed number as a quantity of bitcoins, and instances + * of {@link BtcAutoFormat} will interpret the number as a quantity of that instance's + * configured denomination, which can be ascertained by invoking the {@link + * BtcFixedFormat#symbol()} or {@link BtcFixedFormat#code()} method. + * + * @return a Coin object representing the parsed value + */ + public Coin parse(String source) throws ParseException { + return (Coin)parseObject(source); + } + + /*********************************/ + /****** END OF PARSING STUFF *****/ + /*********************************/ + + protected static String prefixCode(String code, int scale) { + switch (scale) { + case COIN_SCALE: return code; + case 1: return "d" + code; + case 2: return "c" + code; + case MILLICOIN_SCALE: return "m" + code; + case MICROCOIN_SCALE: return "µ" + code; + case -1: return "da" + code; + case -2: return "h" + code; + case -3: return "k" + code; + case -6: return "M" + code; + default: throw new IllegalStateException("No known prefix for scale " + String.valueOf(scale)); + } + } + + protected static String prefixSymbol(String symbol, int scale) { + switch (scale) { + case COIN_SCALE: return symbol; + case 1: return "d" + symbol; + case 2: return "¢" + symbol; + case MILLICOIN_SCALE: return "₥" + symbol; + case MICROCOIN_SCALE: return "µ" + symbol; + case -1: return "da" + symbol; + case -2: return "h" + symbol; + case -3: return "k" + symbol; + case -6: return "M" + symbol; + default: throw new IllegalStateException("No known prefix for scale " + String.valueOf(scale)); + } + } + + /** Guarantee a formatting pattern has a subpattern for negative values. This method takes + * a pattern that may be missing a negative subpattern, and returns the same pattern with + * a negative subpattern appended as needed. + * + *

    This method accommodates an imperfection in the Java formatting code and distributed + * locale data. To wit: the subpattern for negative numbers is optional and not all + * locales have one. In those cases, {@link java.text.DecimalFormat} will indicate numbers + * less than zero by adding a negative sign as the first character of the prefix of the + * positive subpattern. + * + *

    We don't like this, since we claim the negative sign applies to the number not the + * units, and therefore it ought to be adjacent to the number, displacing the + * currency-units indicator if necessary. + */ + protected static String negify(String pattern) { + if (pattern.contains(";")) return pattern; + else { + if (pattern.contains("-")) + throw new IllegalStateException("Positive pattern contains negative sign"); + // the regex matches everything until the first non-quoted number character + return pattern + ";" + pattern.replaceFirst("^([^#0,.']*('[^']*')?)*", "$0-"); + } + } + + /** + * Return an array of all locales for which the getInstance() method of this class can + * return localized instances. See {@link java.text.NumberFormat#getAvailableLocales()} + */ + public static Locale[] getAvailableLocales() { return NumberFormat.getAvailableLocales(); } + + /** Return the unprefixed currency symbol for bitcoins configured for this object. The + * return value of this method is constant throughough the life of an instance. */ + public String coinSymbol() { synchronized(numberFormat) { + return numberFormat.getDecimalFormatSymbols().getCurrencySymbol(); + }} + + /** Return the unprefixed international currency code for bitcoins configured for this + * object. The return value of this method is constant throughough the life of an instance. */ + public String coinCode() { synchronized(numberFormat) { + return numberFormat.getDecimalFormatSymbols().getInternationalCurrencySymbol(); + }} + + /** Return a representation of the pattern used by this instance for formatting and + * parsing. The format is similar to, but not the same as the format recognized by the + * {@link Builder.pattern()} and {@link Builder.localizedPattern()} methods. The pattern + * returned by this method is localized, any currency signs expressed are literally, and + * optional fractional decimal places are shown grouped in parentheses. */ + public String pattern() { synchronized(numberFormat) { + StringBuffer groups = new StringBuffer(); + for (int group : decimalGroups) { + groups.append("(" + Strings.repeat("#",group) + ")"); + } + DecimalFormatSymbols s = numberFormat.getDecimalFormatSymbols(); + String digit = String.valueOf(s.getDigit()); + String exp = s.getExponentSeparator(); + String groupSep = String.valueOf(s.getGroupingSeparator()); + String moneySep = String.valueOf(s.getMonetaryDecimalSeparator()); + String zero = String.valueOf(s.getZeroDigit()); + String boundary = String.valueOf(s.getPatternSeparator()); + String minus = String.valueOf(s.getMinusSign()); + String decSep = String.valueOf(s.getDecimalSeparator()); + + String prefixAndNumber = "(^|" + boundary+ ")" + + "([^" + Matcher.quoteReplacement(digit + zero + groupSep + decSep + moneySep) + "']*('[^']*')?)*" + + "[" + Matcher.quoteReplacement(digit + zero + groupSep + decSep + moneySep + exp) + "]+"; + + return numberFormat.toLocalizedPattern(). + replaceAll(prefixAndNumber, "$0" + groups.toString()). + replaceAll("¤¤", Matcher.quoteReplacement(coinCode())). + replaceAll("¤", Matcher.quoteReplacement(coinSymbol())); + }} + + /** Return a copy of the localized symbols used by this instance for formatting and parsing. */ + public DecimalFormatSymbols symbols() { synchronized(numberFormat) { + return numberFormat.getDecimalFormatSymbols(); + }} + + /** 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 BtcFormat)) return false; + BtcFormat other = (BtcFormat)o; + return other.pattern().equals(pattern()) && + other.symbols().equals(symbols()) && + other.minimumFractionDigits == minimumFractionDigits; + } + + /** Return a hash code value for this instance. + * @see java.lang.Object#hashCode + */ + @Override public int hashCode() { + int result = 17; + result = 31 * result + pattern().hashCode(); + result = 31 * result + symbols().hashCode(); + result = 31 * result + minimumFractionDigits; + result = 31 * result + decimalGroups.hashCode(); + return result; + } + +} diff --git a/core/src/test/java/com/google/bitcoin/utils/BtcFormatTest.java b/core/src/test/java/com/google/bitcoin/utils/BtcFormatTest.java new file mode 100644 index 00000000..f36a2c74 --- /dev/null +++ b/core/src/test/java/com/google/bitcoin/utils/BtcFormatTest.java @@ -0,0 +1,1497 @@ +/* + * 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 org.junit.Test; +import org.junit.Rule; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import org.junit.rules.ExpectedException; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import org.junit.runner.RunWith; + +import java.math.BigDecimal; +import java.text.AttributedCharacterIterator; +import java.text.AttributedCharacterIterator.Attribute; +import java.text.CharacterIterator; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.text.FieldPosition; +import java.text.NumberFormat; +import java.text.ParseException; +import static java.text.NumberFormat.Field.DECIMAL_SEPARATOR; + +import java.util.Locale; +import static java.util.Locale.*; +import java.util.Currency; +import java.util.Set; +import java.util.HashSet; + +import static com.google.common.base.Strings.repeat; + +import com.google.bitcoin.core.Coin; +import static com.google.bitcoin.core.NetworkParameters.MAX_MONEY; +import static com.google.bitcoin.core.Coin.parseCoin; +import static com.google.bitcoin.core.Coin.valueOf; +import static com.google.bitcoin.core.Coin.COIN; +import static com.google.bitcoin.core.Coin.SMALLEST_UNIT_EXPONENT; +import static com.google.bitcoin.core.Coin.SATOSHI; +import static com.google.bitcoin.core.Coin.ZERO; +import static com.google.bitcoin.utils.BtcFixedFormat.*; +import static com.google.bitcoin.utils.BtcAutoFormat.Style.*; + +@RunWith(Parameterized.class) +public class BtcFormatTest { + + @Parameters + public static Set data() { + Set localeSet = new HashSet(); + for (Locale locale : Locale.getAvailableLocales()) { + localeSet.add(new Locale[]{locale}); + } + return localeSet; + } + + public BtcFormatTest(Locale defaultLocale) { + Locale.setDefault(defaultLocale); + } + + @Test + public void prefixTest() { // prefix b/c symbol is prefixed + BtcFormat usFormat = BtcFormat.getSymbolInstance(Locale.US); + assertEquals("฿1.00", usFormat.format(COIN)); + assertEquals("฿1.01", usFormat.format(101000000)); + assertEquals("₥฿0.01", usFormat.format(1000)); + assertEquals("₥฿1,011.00", usFormat.format(101100000)); + assertEquals("₥฿1,000.01", usFormat.format(100001000)); + assertEquals("µ฿1,000,001.00", usFormat.format(100000100)); + assertEquals("µ฿1,000,000.10", usFormat.format(100000010)); + assertEquals("µ฿1,000,000.01", usFormat.format(100000001)); + assertEquals("µ฿1.00", usFormat.format(100)); + assertEquals("µ฿0.10", usFormat.format(10)); + assertEquals("µ฿0.01", usFormat.format(1)); + } + + @Test + public void suffixTest() { + BtcFormat deFormat = BtcFormat.getSymbolInstance(Locale.GERMANY); + // int + assertEquals("1,00 ฿", deFormat.format(100000000)); + assertEquals("1,01 ฿", deFormat.format(101000000)); + assertEquals("1.011,00 ₥฿", deFormat.format(101100000)); + assertEquals("1.000,01 ₥฿", deFormat.format(100001000)); + assertEquals("1.000.001,00 µ฿", deFormat.format(100000100)); + assertEquals("1.000.000,10 µ฿", deFormat.format(100000010)); + assertEquals("1.000.000,01 µ฿", deFormat.format(100000001)); + } + + @Test + public void defaultLocaleTest() { + assertEquals( + "Default Locale is " + Locale.getDefault().toString(), + BtcFormat.getInstance().pattern(), BtcFormat.getInstance(Locale.getDefault()).pattern() + ); + assertEquals( + "Default Locale is " + Locale.getDefault().toString(), + BtcFormat.getCodeInstance().pattern(), + BtcFormat.getCodeInstance(Locale.getDefault()).pattern() + ); + } + + @Test + public void symbolCollisionTest() { + Locale[] locales = BtcFormat.getAvailableLocales(); + for (int i = 0; i < locales.length; ++i) { + String cs = ((DecimalFormat)NumberFormat.getCurrencyInstance(locales[i])). + getDecimalFormatSymbols().getCurrencySymbol(); + if (cs.contains("฿")) { + BtcFormat bf = BtcFormat.getSymbolInstance(locales[i]); + String coin = bf.format(COIN); + assertTrue(coin.contains("Ƀ")); + assertFalse(coin.contains("฿")); + String milli = bf.format(valueOf(10000)); + assertTrue(milli.contains("₥Ƀ")); + assertFalse(milli.contains("฿")); + String micro = bf.format(valueOf(100)); + assertTrue(micro.contains("µɃ")); + assertFalse(micro.contains("฿")); + BtcFormat ff = BtcFormat.builder().scale(0).locale(locales[i]).pattern("¤#.#").build(); + assertEquals("Ƀ", ((BtcFixedFormat)ff).symbol()); + assertEquals("Ƀ", ff.coinSymbol()); + coin = ff.format(COIN); + assertTrue(coin.contains("Ƀ")); + assertFalse(coin.contains("฿")); + BtcFormat mlff = BtcFormat.builder().scale(3).locale(locales[i]).pattern("¤#.#").build(); + assertEquals("₥Ƀ", ((BtcFixedFormat)mlff).symbol()); + assertEquals("Ƀ", mlff.coinSymbol()); + milli = mlff.format(valueOf(10000)); + assertTrue(milli.contains("₥Ƀ")); + assertFalse(milli.contains("฿")); + BtcFormat mcff = BtcFormat.builder().scale(6).locale(locales[i]).pattern("¤#.#").build(); + assertEquals("µɃ", ((BtcFixedFormat)mcff).symbol()); + assertEquals("Ƀ", mcff.coinSymbol()); + micro = mcff.format(valueOf(100)); + assertTrue(micro.contains("µɃ")); + assertFalse(micro.contains("฿")); + } + if (cs.contains("Ƀ")) { // NB: We don't know of any such existing locale, but check anyway. + BtcFormat bf = BtcFormat.getInstance(locales[i]); + String coin = bf.format(COIN); + assertTrue(coin.contains("฿")); + assertFalse(coin.contains("Ƀ")); + String milli = bf.format(valueOf(10000)); + assertTrue(milli.contains("₥฿")); + assertFalse(milli.contains("Ƀ")); + String micro = bf.format(valueOf(100)); + assertTrue(micro.contains("µ฿")); + assertFalse(micro.contains("Ƀ")); + } + } + } + + @Test + public void argumentTypeTest() { + BtcFormat usFormat = BtcFormat.getSymbolInstance(Locale.US); + // longs are tested above + // Coin + assertEquals("µ฿1,000,000.01", usFormat.format(COIN.add(valueOf(1)))); + // Integer + assertEquals("µ฿21,474,836.47" ,usFormat.format(Integer.MAX_VALUE)); + assertEquals("(µ฿21,474,836.48)" ,usFormat.format(Integer.MIN_VALUE)); + // Long + assertEquals("µ฿92,233,720,368,547,758.07" ,usFormat.format(Long.MAX_VALUE)); + assertEquals("(µ฿92,233,720,368,547,758.08)" ,usFormat.format(Long.MIN_VALUE)); + // BigInteger + assertEquals("µ฿0.10" ,usFormat.format(java.math.BigInteger.TEN)); + assertEquals("฿0.00" ,usFormat.format(java.math.BigInteger.ZERO)); + // BigDecimal + assertEquals("฿1.00" ,usFormat.format(java.math.BigDecimal.ONE)); + assertEquals("฿0.00" ,usFormat.format(java.math.BigDecimal.ZERO)); + // use of Double not encouraged but no way to stop user from converting one to BigDecimal + assertEquals( + "฿179,769,313,486,231,570,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000.00", + usFormat.format(java.math.BigDecimal.valueOf(Double.MAX_VALUE))); + assertEquals("฿0.00", usFormat.format(java.math.BigDecimal.valueOf(Double.MIN_VALUE))); + assertEquals( + "฿340,282,346,638,528,860,000,000,000,000,000,000,000.00", + usFormat.format(java.math.BigDecimal.valueOf(Float.MAX_VALUE))); + // Bad type + try { + usFormat.format("1"); + fail("should not have tried to format a String"); + } catch (IllegalArgumentException e) { + } + } + + @Test + public void columnAlignmentTest() { + BtcFormat germany = BtcFormat.getCoinInstance(2,BtcFixedFormat.REPEATING_PLACES); + char separator = germany.symbols().getDecimalSeparator(); + Coin[] rows = {MAX_MONEY, MAX_MONEY.subtract(SATOSHI), Coin.parseCoin("1234"), + COIN, COIN.add(SATOSHI), COIN.subtract(SATOSHI), + COIN.divide(1000).add(SATOSHI), COIN.divide(1000), COIN.divide(1000).subtract(SATOSHI), + valueOf(100), valueOf(1000), valueOf(10000), + SATOSHI}; + FieldPosition fp = new FieldPosition(DECIMAL_SEPARATOR); + String[] output = new String[rows.length]; + int[] indexes = new int[rows.length]; + int maxIndex = 0; + for (int i = 0; i < rows.length; i++) { + output[i] = germany.format(rows[i], new StringBuffer(), fp).toString(); + indexes[i] = fp.getBeginIndex(); + if (indexes[i] > maxIndex) maxIndex = indexes[i]; + } + for (int i = 0; i < output.length; i++) { + // uncomment to watch printout + // System.out.println(repeat(" ", (maxIndex - indexes[i])) + output[i]); + assertEquals(output[i].indexOf(separator), indexes[i]); + } + } + + @Test + public void repeatingPlaceTest() { + BtcFormat mega = BtcFormat.getInstance(-6, US); + Coin value = MAX_MONEY.subtract(SATOSHI); + assertEquals("20.99999999999999", mega.format(value, 0, BtcFixedFormat.REPEATING_PLACES)); + assertEquals("20.99999999999999", mega.format(value, 0, BtcFixedFormat.REPEATING_PLACES)); + assertEquals("20.99999999999999", mega.format(value, 1, BtcFixedFormat.REPEATING_PLACES)); + assertEquals("20.99999999999999", mega.format(value, 2, BtcFixedFormat.REPEATING_PLACES)); + assertEquals("20.99999999999999", mega.format(value, 3, BtcFixedFormat.REPEATING_PLACES)); + assertEquals("20.99999999999999", mega.format(value, 0, BtcFixedFormat.REPEATING_DOUBLETS)); + assertEquals("20.99999999999999", mega.format(value, 1, BtcFixedFormat.REPEATING_DOUBLETS)); + assertEquals("20.99999999999999", mega.format(value, 2, BtcFixedFormat.REPEATING_DOUBLETS)); + assertEquals("20.99999999999999", mega.format(value, 3, BtcFixedFormat.REPEATING_DOUBLETS)); + assertEquals("20.99999999999999", mega.format(value, 0, BtcFixedFormat.REPEATING_TRIPLETS)); + assertEquals("20.99999999999999", mega.format(value, 1, BtcFixedFormat.REPEATING_TRIPLETS)); + assertEquals("20.99999999999999", mega.format(value, 2, BtcFixedFormat.REPEATING_TRIPLETS)); + assertEquals("20.99999999999999", mega.format(value, 3, BtcFixedFormat.REPEATING_TRIPLETS)); + assertEquals("1.00000005", BtcFormat.getCoinInstance(US). + format(COIN.add(Coin.valueOf(5)), 0, BtcFixedFormat.REPEATING_PLACES)); + } + + @Test + public void characterIteratorTest() { + BtcFormat usFormat = BtcFormat.getInstance(Locale.US); + AttributedCharacterIterator i = usFormat.formatToCharacterIterator(parseCoin("1234.5")); + java.util.Set a = i.getAllAttributeKeys(); + assertTrue("Missing currency attribute", a.contains(NumberFormat.Field.CURRENCY)); + assertTrue("Missing integer attribute", a.contains(NumberFormat.Field.INTEGER)); + assertTrue("Missing fraction attribute", a.contains(NumberFormat.Field.FRACTION)); + assertTrue("Missing decimal separator attribute", a.contains(NumberFormat.Field.DECIMAL_SEPARATOR)); + assertTrue("Missing grouping separator attribute", a.contains(NumberFormat.Field.GROUPING_SEPARATOR)); + assertTrue("Missing currency attribute", a.contains(NumberFormat.Field.CURRENCY)); + + char c; + i = BtcFormat.getCodeInstance(Locale.US).formatToCharacterIterator(new BigDecimal("0.19246362747414458")); + // formatted as "µBTC 192,463.63" + assertEquals(0, i.getBeginIndex()); + assertEquals(15, i.getEndIndex()); + int n = 0; + for(c = i.first(); i.getAttribute(NumberFormat.Field.CURRENCY) != null; c = i.next()) { + n++; + } + assertEquals(4, n); + n = 0; + for(i.next(); i.getAttribute(NumberFormat.Field.INTEGER) != null && i.getAttribute(NumberFormat.Field.GROUPING_SEPARATOR) != NumberFormat.Field.GROUPING_SEPARATOR; c = i.next()) { + n++; + } + assertEquals(3, n); + assertEquals(NumberFormat.Field.INTEGER, i.getAttribute(NumberFormat.Field.INTEGER)); + n = 0; + for(c = i.next(); i.getAttribute(NumberFormat.Field.INTEGER) != null; c = i.next()) { + n++; + } + assertEquals(3, n); + assertEquals(NumberFormat.Field.DECIMAL_SEPARATOR, i.getAttribute(NumberFormat.Field.DECIMAL_SEPARATOR)); + n = 0; + for(c = i.next(); c != CharacterIterator.DONE; c = i.next()) { + n++; + assertNotNull(i.getAttribute(NumberFormat.Field.FRACTION)); + } + assertEquals(2,n); + + // immutability check + BtcFormat fa = BtcFormat.getSymbolInstance(US); + BtcFormat fb = BtcFormat.getSymbolInstance(US); + assertEquals(fa, fb); + assertEquals(fa.hashCode(), fb.hashCode()); + fa.formatToCharacterIterator(COIN.multiply(1000000)); + assertEquals(fa, fb); + assertEquals(fa.hashCode(), fb.hashCode()); + fb.formatToCharacterIterator(COIN.divide(1000000)); + assertEquals(fa, fb); + assertEquals(fa.hashCode(), fb.hashCode()); + } + + @Test + public void parseTest() throws java.text.ParseException { + BtcFormat us = BtcFormat.getSymbolInstance(Locale.US); + BtcFormat usCoded = BtcFormat.getCodeInstance(Locale.US); + // Coins + assertEquals(valueOf(200000000), us.parseObject("BTC2")); + assertEquals(valueOf(200000000), us.parseObject("XBT2")); + assertEquals(valueOf(200000000), us.parseObject("฿2")); + assertEquals(valueOf(200000000), us.parseObject("Ƀ2")); + assertEquals(valueOf(200000000), us.parseObject("2")); + assertEquals(valueOf(200000000), usCoded.parseObject("BTC 2")); + assertEquals(valueOf(200000000), usCoded.parseObject("XBT 2")); + assertEquals(valueOf(200000000), us.parseObject("฿2.0")); + assertEquals(valueOf(200000000), us.parseObject("Ƀ2.0")); + assertEquals(valueOf(200000000), us.parseObject("2.0")); + assertEquals(valueOf(200000000), us.parseObject("BTC2.0")); + assertEquals(valueOf(200000000), us.parseObject("XBT2.0")); + assertEquals(valueOf(200000000), usCoded.parseObject("฿ 2")); + assertEquals(valueOf(200000000), usCoded.parseObject("Ƀ 2")); + assertEquals(valueOf(200000000), usCoded.parseObject(" 2")); + assertEquals(valueOf(200000000), usCoded.parseObject("BTC 2")); + assertEquals(valueOf(200000000), usCoded.parseObject("XBT 2")); + assertEquals(valueOf(202222420000000L), us.parseObject("2,022,224.20")); + assertEquals(valueOf(202222420000000L), us.parseObject("฿2,022,224.20")); + assertEquals(valueOf(202222420000000L), us.parseObject("Ƀ2,022,224.20")); + assertEquals(valueOf(202222420000000L), us.parseObject("BTC2,022,224.20")); + assertEquals(valueOf(202222420000000L), us.parseObject("XBT2,022,224.20")); + assertEquals(valueOf(220200000000L), us.parseObject("2,202.0")); + assertEquals(valueOf(2100000000000000L), us.parseObject("21000000.00000000")); + // MilliCoins + assertEquals(valueOf(200000), usCoded.parseObject("mBTC 2")); + assertEquals(valueOf(200000), usCoded.parseObject("mXBT 2")); + assertEquals(valueOf(200000), usCoded.parseObject("m฿ 2")); + assertEquals(valueOf(200000), usCoded.parseObject("mɃ 2")); + assertEquals(valueOf(200000), us.parseObject("mBTC2")); + assertEquals(valueOf(200000), us.parseObject("mXBT2")); + assertEquals(valueOf(200000), us.parseObject("₥฿2")); + assertEquals(valueOf(200000), us.parseObject("₥Ƀ2")); + assertEquals(valueOf(200000), us.parseObject("₥2")); + assertEquals(valueOf(200000), usCoded.parseObject("₥BTC 2.00")); + assertEquals(valueOf(200000), usCoded.parseObject("₥XBT 2.00")); + assertEquals(valueOf(200000), usCoded.parseObject("₥BTC 2")); + assertEquals(valueOf(200000), usCoded.parseObject("₥XBT 2")); + assertEquals(valueOf(200000), usCoded.parseObject("₥฿ 2")); + assertEquals(valueOf(200000), usCoded.parseObject("₥Ƀ 2")); + assertEquals(valueOf(200000), usCoded.parseObject("₥ 2")); + assertEquals(valueOf(202222400000L), us.parseObject("₥฿2,022,224")); + assertEquals(valueOf(202222420000L), us.parseObject("₥Ƀ2,022,224.20")); + assertEquals(valueOf(202222400000L), us.parseObject("m฿2,022,224")); + assertEquals(valueOf(202222420000L), us.parseObject("mɃ2,022,224.20")); + assertEquals(valueOf(202222400000L), us.parseObject("₥BTC2,022,224")); + assertEquals(valueOf(202222400000L), us.parseObject("₥XBT2,022,224")); + assertEquals(valueOf(202222400000L), us.parseObject("mBTC2,022,224")); + assertEquals(valueOf(202222400000L), us.parseObject("mXBT2,022,224")); + assertEquals(valueOf(202222420000L), us.parseObject("₥2,022,224.20")); + assertEquals(valueOf(202222400000L), usCoded.parseObject("₥฿ 2,022,224")); + assertEquals(valueOf(202222420000L), usCoded.parseObject("₥Ƀ 2,022,224.20")); + assertEquals(valueOf(202222400000L), usCoded.parseObject("m฿ 2,022,224")); + assertEquals(valueOf(202222420000L), usCoded.parseObject("mɃ 2,022,224.20")); + assertEquals(valueOf(202222400000L), usCoded.parseObject("₥BTC 2,022,224")); + assertEquals(valueOf(202222400000L), usCoded.parseObject("₥XBT 2,022,224")); + assertEquals(valueOf(202222400000L), usCoded.parseObject("mBTC 2,022,224")); + assertEquals(valueOf(202222400000L), usCoded.parseObject("mXBT 2,022,224")); + assertEquals(valueOf(202222420000L), usCoded.parseObject("₥ 2,022,224.20")); + // Microcoins + assertEquals(valueOf(435), us.parseObject("µ฿4.35")); + assertEquals(valueOf(435), us.parseObject("uɃ4.35")); + assertEquals(valueOf(435), us.parseObject("u฿4.35")); + assertEquals(valueOf(435), us.parseObject("µɃ4.35")); + assertEquals(valueOf(435), us.parseObject("uBTC4.35")); + assertEquals(valueOf(435), us.parseObject("uXBT4.35")); + assertEquals(valueOf(435), us.parseObject("µBTC4.35")); + assertEquals(valueOf(435), us.parseObject("µXBT4.35")); + assertEquals(valueOf(435), usCoded.parseObject("uBTC 4.35")); + assertEquals(valueOf(435), usCoded.parseObject("uXBT 4.35")); + assertEquals(valueOf(435), usCoded.parseObject("µBTC 4.35")); + assertEquals(valueOf(435), usCoded.parseObject("µXBT 4.35")); + // fractional satoshi; round up + assertEquals(valueOf(435), us.parseObject("uBTC4.345")); + assertEquals(valueOf(435), us.parseObject("uXBT4.345")); + // negative with mu symbol + assertEquals(valueOf(-1), usCoded.parseObject("(µ฿ 0.01)")); + assertEquals(valueOf(-10), us.parseObject("(µBTC0.100)")); + assertEquals(valueOf(-10), us.parseObject("(µXBT0.100)")); + + // Same thing with addition of custom code, symbol + us = BtcFormat.builder().locale(US).style(SYMBOL).symbol("£").code("XYZ").build(); + usCoded = BtcFormat.builder().locale(US).scale(0).symbol("£").code("XYZ"). + pattern("¤ #,##0.00").build(); + // Coins + assertEquals(valueOf(200000000), us.parseObject("XYZ2")); + assertEquals(valueOf(200000000), us.parseObject("BTC2")); + assertEquals(valueOf(200000000), us.parseObject("XBT2")); + assertEquals(valueOf(200000000), us.parseObject("£2")); + assertEquals(valueOf(200000000), us.parseObject("฿2")); + assertEquals(valueOf(200000000), us.parseObject("Ƀ2")); + assertEquals(valueOf(200000000), us.parseObject("2")); + assertEquals(valueOf(200000000), usCoded.parseObject("XYZ 2")); + assertEquals(valueOf(200000000), usCoded.parseObject("BTC 2")); + assertEquals(valueOf(200000000), usCoded.parseObject("XBT 2")); + assertEquals(valueOf(200000000), us.parseObject("£2.0")); + assertEquals(valueOf(200000000), us.parseObject("฿2.0")); + assertEquals(valueOf(200000000), us.parseObject("Ƀ2.0")); + assertEquals(valueOf(200000000), us.parseObject("2.0")); + assertEquals(valueOf(200000000), us.parseObject("XYZ2.0")); + assertEquals(valueOf(200000000), us.parseObject("BTC2.0")); + assertEquals(valueOf(200000000), us.parseObject("XBT2.0")); + assertEquals(valueOf(200000000), usCoded.parseObject("£ 2")); + assertEquals(valueOf(200000000), usCoded.parseObject("฿ 2")); + assertEquals(valueOf(200000000), usCoded.parseObject("Ƀ 2")); + assertEquals(valueOf(200000000), usCoded.parseObject(" 2")); + assertEquals(valueOf(200000000), usCoded.parseObject("XYZ 2")); + assertEquals(valueOf(200000000), usCoded.parseObject("BTC 2")); + assertEquals(valueOf(200000000), usCoded.parseObject("XBT 2")); + assertEquals(valueOf(202222420000000L), us.parseObject("2,022,224.20")); + assertEquals(valueOf(202222420000000L), us.parseObject("£2,022,224.20")); + assertEquals(valueOf(202222420000000L), us.parseObject("฿2,022,224.20")); + assertEquals(valueOf(202222420000000L), us.parseObject("Ƀ2,022,224.20")); + assertEquals(valueOf(202222420000000L), us.parseObject("XYZ2,022,224.20")); + assertEquals(valueOf(202222420000000L), us.parseObject("BTC2,022,224.20")); + assertEquals(valueOf(202222420000000L), us.parseObject("XBT2,022,224.20")); + assertEquals(valueOf(220200000000L), us.parseObject("2,202.0")); + assertEquals(valueOf(2100000000000000L), us.parseObject("21000000.00000000")); + // MilliCoins + assertEquals(valueOf(200000), usCoded.parseObject("mXYZ 2")); + assertEquals(valueOf(200000), usCoded.parseObject("mBTC 2")); + assertEquals(valueOf(200000), usCoded.parseObject("mXBT 2")); + assertEquals(valueOf(200000), usCoded.parseObject("m£ 2")); + assertEquals(valueOf(200000), usCoded.parseObject("m฿ 2")); + assertEquals(valueOf(200000), usCoded.parseObject("mɃ 2")); + assertEquals(valueOf(200000), us.parseObject("mXYZ2")); + assertEquals(valueOf(200000), us.parseObject("mBTC2")); + assertEquals(valueOf(200000), us.parseObject("mXBT2")); + assertEquals(valueOf(200000), us.parseObject("₥£2")); + assertEquals(valueOf(200000), us.parseObject("₥฿2")); + assertEquals(valueOf(200000), us.parseObject("₥Ƀ2")); + assertEquals(valueOf(200000), us.parseObject("₥2")); + assertEquals(valueOf(200000), usCoded.parseObject("₥XYZ 2.00")); + assertEquals(valueOf(200000), usCoded.parseObject("₥BTC 2.00")); + assertEquals(valueOf(200000), usCoded.parseObject("₥XBT 2.00")); + assertEquals(valueOf(200000), usCoded.parseObject("₥XYZ 2")); + assertEquals(valueOf(200000), usCoded.parseObject("₥BTC 2")); + assertEquals(valueOf(200000), usCoded.parseObject("₥XBT 2")); + assertEquals(valueOf(200000), usCoded.parseObject("₥£ 2")); + assertEquals(valueOf(200000), usCoded.parseObject("₥฿ 2")); + assertEquals(valueOf(200000), usCoded.parseObject("₥Ƀ 2")); + assertEquals(valueOf(200000), usCoded.parseObject("₥ 2")); + assertEquals(valueOf(202222400000L), us.parseObject("₥£2,022,224")); + assertEquals(valueOf(202222400000L), us.parseObject("₥฿2,022,224")); + assertEquals(valueOf(202222420000L), us.parseObject("₥Ƀ2,022,224.20")); + assertEquals(valueOf(202222400000L), us.parseObject("m£2,022,224")); + assertEquals(valueOf(202222400000L), us.parseObject("m฿2,022,224")); + assertEquals(valueOf(202222420000L), us.parseObject("mɃ2,022,224.20")); + assertEquals(valueOf(202222400000L), us.parseObject("₥XYZ2,022,224")); + assertEquals(valueOf(202222400000L), us.parseObject("₥BTC2,022,224")); + assertEquals(valueOf(202222400000L), us.parseObject("₥XBT2,022,224")); + assertEquals(valueOf(202222400000L), us.parseObject("mXYZ2,022,224")); + assertEquals(valueOf(202222400000L), us.parseObject("mBTC2,022,224")); + assertEquals(valueOf(202222400000L), us.parseObject("mXBT2,022,224")); + assertEquals(valueOf(202222420000L), us.parseObject("₥2,022,224.20")); + assertEquals(valueOf(202222400000L), usCoded.parseObject("₥£ 2,022,224")); + assertEquals(valueOf(202222400000L), usCoded.parseObject("₥฿ 2,022,224")); + assertEquals(valueOf(202222420000L), usCoded.parseObject("₥Ƀ 2,022,224.20")); + assertEquals(valueOf(202222400000L), usCoded.parseObject("m£ 2,022,224")); + assertEquals(valueOf(202222400000L), usCoded.parseObject("m฿ 2,022,224")); + assertEquals(valueOf(202222420000L), usCoded.parseObject("mɃ 2,022,224.20")); + assertEquals(valueOf(202222400000L), usCoded.parseObject("₥XYZ 2,022,224")); + assertEquals(valueOf(202222400000L), usCoded.parseObject("₥BTC 2,022,224")); + assertEquals(valueOf(202222400000L), usCoded.parseObject("₥XBT 2,022,224")); + assertEquals(valueOf(202222400000L), usCoded.parseObject("mXYZ 2,022,224")); + assertEquals(valueOf(202222400000L), usCoded.parseObject("mBTC 2,022,224")); + assertEquals(valueOf(202222400000L), usCoded.parseObject("mXBT 2,022,224")); + assertEquals(valueOf(202222420000L), usCoded.parseObject("₥ 2,022,224.20")); + // Microcoins + assertEquals(valueOf(435), us.parseObject("µ£4.35")); + assertEquals(valueOf(435), us.parseObject("µ฿4.35")); + assertEquals(valueOf(435), us.parseObject("uɃ4.35")); + assertEquals(valueOf(435), us.parseObject("u£4.35")); + assertEquals(valueOf(435), us.parseObject("u฿4.35")); + assertEquals(valueOf(435), us.parseObject("µɃ4.35")); + assertEquals(valueOf(435), us.parseObject("uXYZ4.35")); + assertEquals(valueOf(435), us.parseObject("uBTC4.35")); + assertEquals(valueOf(435), us.parseObject("uXBT4.35")); + assertEquals(valueOf(435), us.parseObject("µXYZ4.35")); + assertEquals(valueOf(435), us.parseObject("µBTC4.35")); + assertEquals(valueOf(435), us.parseObject("µXBT4.35")); + assertEquals(valueOf(435), usCoded.parseObject("uXYZ 4.35")); + assertEquals(valueOf(435), usCoded.parseObject("uBTC 4.35")); + assertEquals(valueOf(435), usCoded.parseObject("uXBT 4.35")); + assertEquals(valueOf(435), usCoded.parseObject("µXYZ 4.35")); + assertEquals(valueOf(435), usCoded.parseObject("µBTC 4.35")); + assertEquals(valueOf(435), usCoded.parseObject("µXBT 4.35")); + // fractional satoshi; round up + assertEquals(valueOf(435), us.parseObject("uXYZ4.345")); + assertEquals(valueOf(435), us.parseObject("uBTC4.345")); + assertEquals(valueOf(435), us.parseObject("uXBT4.345")); + // negative with mu symbol + assertEquals(valueOf(-1), usCoded.parseObject("µ£ -0.01")); + assertEquals(valueOf(-1), usCoded.parseObject("µ฿ -0.01")); + assertEquals(valueOf(-10), us.parseObject("(µXYZ0.100)")); + assertEquals(valueOf(-10), us.parseObject("(µBTC0.100)")); + assertEquals(valueOf(-10), us.parseObject("(µXBT0.100)")); + + // parse() method as opposed to parseObject + try { + BtcFormat.getInstance().parse("abc"); + fail("bad parse must raise exception"); + } catch (ParseException e) {} + } + + @Test + public void parseMetricTest() throws ParseException { + BtcFormat cp = BtcFormat.getCodeInstance(Locale.US); + BtcFormat sp = BtcFormat.getSymbolInstance(Locale.US); + // coin + assertEquals(parseCoin("1"), cp.parseObject("BTC 1.00")); + assertEquals(parseCoin("1"), sp.parseObject("BTC1.00")); + assertEquals(parseCoin("1"), cp.parseObject("฿ 1.00")); + assertEquals(parseCoin("1"), sp.parseObject("฿1.00")); + assertEquals(parseCoin("1"), cp.parseObject("B⃦ 1.00")); + assertEquals(parseCoin("1"), sp.parseObject("B⃦1.00")); + assertEquals(parseCoin("1"), cp.parseObject("Ƀ 1.00")); + assertEquals(parseCoin("1"), sp.parseObject("Ƀ1.00")); + // milli + assertEquals(parseCoin("0.001"), cp.parseObject("mBTC 1.00")); + assertEquals(parseCoin("0.001"), sp.parseObject("mBTC1.00")); + assertEquals(parseCoin("0.001"), cp.parseObject("m฿ 1.00")); + assertEquals(parseCoin("0.001"), sp.parseObject("m฿1.00")); + assertEquals(parseCoin("0.001"), cp.parseObject("mB⃦ 1.00")); + assertEquals(parseCoin("0.001"), sp.parseObject("mB⃦1.00")); + assertEquals(parseCoin("0.001"), cp.parseObject("mɃ 1.00")); + assertEquals(parseCoin("0.001"), sp.parseObject("mɃ1.00")); + assertEquals(parseCoin("0.001"), cp.parseObject("₥BTC 1.00")); + assertEquals(parseCoin("0.001"), sp.parseObject("₥BTC1.00")); + assertEquals(parseCoin("0.001"), cp.parseObject("₥฿ 1.00")); + assertEquals(parseCoin("0.001"), sp.parseObject("₥฿1.00")); + assertEquals(parseCoin("0.001"), cp.parseObject("₥B⃦ 1.00")); + assertEquals(parseCoin("0.001"), sp.parseObject("₥B⃦1.00")); + assertEquals(parseCoin("0.001"), cp.parseObject("₥Ƀ 1.00")); + assertEquals(parseCoin("0.001"), sp.parseObject("₥Ƀ1.00")); + // micro + assertEquals(parseCoin("0.000001"), cp.parseObject("uBTC 1.00")); + assertEquals(parseCoin("0.000001"), sp.parseObject("uBTC1.00")); + assertEquals(parseCoin("0.000001"), cp.parseObject("u฿ 1.00")); + assertEquals(parseCoin("0.000001"), sp.parseObject("u฿1.00")); + assertEquals(parseCoin("0.000001"), cp.parseObject("uB⃦ 1.00")); + assertEquals(parseCoin("0.000001"), sp.parseObject("uB⃦1.00")); + assertEquals(parseCoin("0.000001"), cp.parseObject("uɃ 1.00")); + assertEquals(parseCoin("0.000001"), sp.parseObject("uɃ1.00")); + assertEquals(parseCoin("0.000001"), cp.parseObject("µBTC 1.00")); + assertEquals(parseCoin("0.000001"), sp.parseObject("µBTC1.00")); + assertEquals(parseCoin("0.000001"), cp.parseObject("µ฿ 1.00")); + assertEquals(parseCoin("0.000001"), sp.parseObject("µ฿1.00")); + assertEquals(parseCoin("0.000001"), cp.parseObject("µB⃦ 1.00")); + assertEquals(parseCoin("0.000001"), sp.parseObject("µB⃦1.00")); + assertEquals(parseCoin("0.000001"), cp.parseObject("µɃ 1.00")); + assertEquals(parseCoin("0.000001"), sp.parseObject("µɃ1.00")); + // satoshi + assertEquals(parseCoin("0.00000001"), cp.parseObject("uBTC 0.01")); + assertEquals(parseCoin("0.00000001"), sp.parseObject("uBTC0.01")); + assertEquals(parseCoin("0.00000001"), cp.parseObject("u฿ 0.01")); + assertEquals(parseCoin("0.00000001"), sp.parseObject("u฿0.01")); + assertEquals(parseCoin("0.00000001"), cp.parseObject("uB⃦ 0.01")); + assertEquals(parseCoin("0.00000001"), sp.parseObject("uB⃦0.01")); + assertEquals(parseCoin("0.00000001"), cp.parseObject("uɃ 0.01")); + assertEquals(parseCoin("0.00000001"), sp.parseObject("uɃ0.01")); + assertEquals(parseCoin("0.00000001"), cp.parseObject("µBTC 0.01")); + assertEquals(parseCoin("0.00000001"), sp.parseObject("µBTC0.01")); + assertEquals(parseCoin("0.00000001"), cp.parseObject("µ฿ 0.01")); + assertEquals(parseCoin("0.00000001"), sp.parseObject("µ฿0.01")); + assertEquals(parseCoin("0.00000001"), cp.parseObject("µB⃦ 0.01")); + assertEquals(parseCoin("0.00000001"), sp.parseObject("µB⃦0.01")); + assertEquals(parseCoin("0.00000001"), cp.parseObject("µɃ 0.01")); + assertEquals(parseCoin("0.00000001"), sp.parseObject("µɃ0.01")); + // cents + assertEquals(parseCoin("0.01234567"), cp.parseObject("cBTC 1.234567")); + assertEquals(parseCoin("0.01234567"), sp.parseObject("cBTC1.234567")); + assertEquals(parseCoin("0.01234567"), cp.parseObject("c฿ 1.234567")); + assertEquals(parseCoin("0.01234567"), sp.parseObject("c฿1.234567")); + assertEquals(parseCoin("0.01234567"), cp.parseObject("cB⃦ 1.234567")); + assertEquals(parseCoin("0.01234567"), sp.parseObject("cB⃦1.234567")); + assertEquals(parseCoin("0.01234567"), cp.parseObject("cɃ 1.234567")); + assertEquals(parseCoin("0.01234567"), sp.parseObject("cɃ1.234567")); + assertEquals(parseCoin("0.01234567"), cp.parseObject("¢BTC 1.234567")); + assertEquals(parseCoin("0.01234567"), sp.parseObject("¢BTC1.234567")); + assertEquals(parseCoin("0.01234567"), cp.parseObject("¢฿ 1.234567")); + assertEquals(parseCoin("0.01234567"), sp.parseObject("¢฿1.234567")); + assertEquals(parseCoin("0.01234567"), cp.parseObject("¢B⃦ 1.234567")); + assertEquals(parseCoin("0.01234567"), sp.parseObject("¢B⃦1.234567")); + assertEquals(parseCoin("0.01234567"), cp.parseObject("¢Ƀ 1.234567")); + assertEquals(parseCoin("0.01234567"), sp.parseObject("¢Ƀ1.234567")); + // dekacoins + assertEquals(parseCoin("12.34567"), cp.parseObject("daBTC 1.234567")); + assertEquals(parseCoin("12.34567"), sp.parseObject("daBTC1.234567")); + assertEquals(parseCoin("12.34567"), cp.parseObject("da฿ 1.234567")); + assertEquals(parseCoin("12.34567"), sp.parseObject("da฿1.234567")); + assertEquals(parseCoin("12.34567"), cp.parseObject("daB⃦ 1.234567")); + assertEquals(parseCoin("12.34567"), sp.parseObject("daB⃦1.234567")); + assertEquals(parseCoin("12.34567"), cp.parseObject("daɃ 1.234567")); + assertEquals(parseCoin("12.34567"), sp.parseObject("daɃ1.234567")); + // hectocoins + assertEquals(parseCoin("123.4567"), cp.parseObject("hBTC 1.234567")); + assertEquals(parseCoin("123.4567"), sp.parseObject("hBTC1.234567")); + assertEquals(parseCoin("123.4567"), cp.parseObject("h฿ 1.234567")); + assertEquals(parseCoin("123.4567"), sp.parseObject("h฿1.234567")); + assertEquals(parseCoin("123.4567"), cp.parseObject("hB⃦ 1.234567")); + assertEquals(parseCoin("123.4567"), sp.parseObject("hB⃦1.234567")); + assertEquals(parseCoin("123.4567"), cp.parseObject("hɃ 1.234567")); + assertEquals(parseCoin("123.4567"), sp.parseObject("hɃ1.234567")); + // kilocoins + assertEquals(parseCoin("1234.567"), cp.parseObject("kBTC 1.234567")); + assertEquals(parseCoin("1234.567"), sp.parseObject("kBTC1.234567")); + assertEquals(parseCoin("1234.567"), cp.parseObject("k฿ 1.234567")); + assertEquals(parseCoin("1234.567"), sp.parseObject("k฿1.234567")); + assertEquals(parseCoin("1234.567"), cp.parseObject("kB⃦ 1.234567")); + assertEquals(parseCoin("1234.567"), sp.parseObject("kB⃦1.234567")); + assertEquals(parseCoin("1234.567"), cp.parseObject("kɃ 1.234567")); + assertEquals(parseCoin("1234.567"), sp.parseObject("kɃ1.234567")); + // megacoins + assertEquals(parseCoin("1234567"), cp.parseObject("MBTC 1.234567")); + assertEquals(parseCoin("1234567"), sp.parseObject("MBTC1.234567")); + assertEquals(parseCoin("1234567"), cp.parseObject("M฿ 1.234567")); + assertEquals(parseCoin("1234567"), sp.parseObject("M฿1.234567")); + assertEquals(parseCoin("1234567"), cp.parseObject("MB⃦ 1.234567")); + assertEquals(parseCoin("1234567"), sp.parseObject("MB⃦1.234567")); + assertEquals(parseCoin("1234567"), cp.parseObject("MɃ 1.234567")); + assertEquals(parseCoin("1234567"), sp.parseObject("MɃ1.234567")); + } + + @Test + public void parsePositionTest() { + BtcFormat usCoded = BtcFormat.getCodeInstance(Locale.US); + // Test the field constants + FieldPosition intField = new FieldPosition(NumberFormat.Field.INTEGER); + assertEquals( + "987,654,321", + usCoded.format(valueOf(98765432123L), new StringBuffer(), intField). + substring(intField.getBeginIndex(), intField.getEndIndex()) + ); + FieldPosition fracField = new FieldPosition(NumberFormat.Field.FRACTION); + assertEquals( + "23", + usCoded.format(valueOf(98765432123L), new StringBuffer(), fracField). + substring(fracField.getBeginIndex(), fracField.getEndIndex()) + ); + + // for currency we use a locale that puts the units at the end + BtcFormat de = BtcFormat.getSymbolInstance(Locale.GERMANY); + BtcFormat deCoded = BtcFormat.getCodeInstance(Locale.GERMANY); + FieldPosition currField = new FieldPosition(NumberFormat.Field.CURRENCY); + assertEquals( + "µ฿", + de.format(valueOf(98765432123L), new StringBuffer(), currField). + substring(currField.getBeginIndex(), currField.getEndIndex()) + ); + assertEquals( + "µBTC", + deCoded.format(valueOf(98765432123L), new StringBuffer(), currField). + substring(currField.getBeginIndex(), currField.getEndIndex()) + ); + assertEquals( + "₥฿", + de.format(valueOf(98765432000L), new StringBuffer(), currField). + substring(currField.getBeginIndex(), currField.getEndIndex()) + ); + assertEquals( + "mBTC", + deCoded.format(valueOf(98765432000L), new StringBuffer(), currField). + substring(currField.getBeginIndex(), currField.getEndIndex()) + ); + assertEquals( + "฿", + de.format(valueOf(98765000000L), new StringBuffer(), currField). + substring(currField.getBeginIndex(), currField.getEndIndex()) + ); + assertEquals( + "BTC", + deCoded.format(valueOf(98765000000L), new StringBuffer(), currField). + substring(currField.getBeginIndex(), currField.getEndIndex()) + ); + } + + @Test + public void currencyCodeTest() { + /* Insert needed space AFTER currency-code */ + BtcFormat usCoded = BtcFormat.getCodeInstance(Locale.US); + assertEquals("µBTC 0.01", usCoded.format(1)); + assertEquals("BTC 1.00", usCoded.format(COIN)); + + /* Do not insert unneeded space BEFORE currency-code */ + BtcFormat frCoded = BtcFormat.getCodeInstance(Locale.FRANCE); + assertEquals("0,01 µBTC", frCoded.format(1)); + assertEquals("1,00 BTC", frCoded.format(COIN)); + + /* Insert needed space BEFORE currency-code: no known currency pattern does this? */ + + /* Do not insert unneeded space AFTER currency-code */ + BtcFormat deCoded = BtcFormat.getCodeInstance(Locale.ITALY); + assertEquals("µBTC 0,01", deCoded.format(1)); + assertEquals("BTC 1,00", deCoded.format(COIN)); + } + + @Test + public void coinScaleTest() throws Exception { + BtcFormat coinFormat = BtcFormat.getCoinInstance(Locale.US); + assertEquals("1.00", coinFormat.format(Coin.COIN)); + assertEquals("-1.00", coinFormat.format(Coin.COIN.negate())); + assertEquals(Coin.parseCoin("1"), coinFormat.parseObject("1.00")); + assertEquals(valueOf(1000000), coinFormat.parseObject("0.01")); + assertEquals(Coin.parseCoin("1000"), coinFormat.parseObject("1,000.00")); + assertEquals(Coin.parseCoin("1000"), coinFormat.parseObject("1000")); + } + + @Test + public void millicoinScaleTest() throws Exception { + BtcFormat coinFormat = BtcFormat.getMilliInstance(Locale.US); + assertEquals("1,000.00", coinFormat.format(Coin.COIN)); + assertEquals("-1,000.00", coinFormat.format(Coin.COIN.negate())); + assertEquals(Coin.parseCoin("0.001"), coinFormat.parseObject("1.00")); + assertEquals(valueOf(1000), coinFormat.parseObject("0.01")); + assertEquals(Coin.parseCoin("1"), coinFormat.parseObject("1,000.00")); + assertEquals(Coin.parseCoin("1"), coinFormat.parseObject("1000")); + } + + @Test + public void microcoinScaleTest() throws Exception { + BtcFormat coinFormat = BtcFormat.getMicroInstance(Locale.US); + assertEquals("1,000,000.00", coinFormat.format(Coin.COIN)); + assertEquals("-1,000,000.00", coinFormat.format(Coin.COIN.negate())); + assertEquals("1,000,000.10", coinFormat.format(Coin.COIN.add(valueOf(10)))); + assertEquals(Coin.parseCoin("0.000001"), coinFormat.parseObject("1.00")); + assertEquals(valueOf(1), coinFormat.parseObject("0.01")); + assertEquals(Coin.parseCoin("0.001"), coinFormat.parseObject("1,000.00")); + assertEquals(Coin.parseCoin("0.001"), coinFormat.parseObject("1000")); + } + + @Test + public void testGrouping() throws Exception { + BtcFormat usCoin = BtcFormat.getInstance(0, Locale.US, 1, 2, 3); + assertEquals("0.1", usCoin.format(Coin.parseCoin("0.1"))); + assertEquals("0.010", usCoin.format(Coin.parseCoin("0.01"))); + assertEquals("0.001", usCoin.format(Coin.parseCoin("0.001"))); + assertEquals("0.000100", usCoin.format(Coin.parseCoin("0.0001"))); + assertEquals("0.000010", usCoin.format(Coin.parseCoin("0.00001"))); + assertEquals("0.000001", usCoin.format(Coin.parseCoin("0.000001"))); + + // no more than two fractional decimal places for the default coin-denomination + assertEquals("0.01", BtcFormat.getCoinInstance(Locale.US).format(Coin.parseCoin("0.005"))); + + BtcFormat usMilli = BtcFormat.getInstance(3, Locale.US, 1, 2, 3); + assertEquals("0.1", usMilli.format(Coin.parseCoin("0.0001"))); + assertEquals("0.010", usMilli.format(Coin.parseCoin("0.00001"))); + assertEquals("0.001", usMilli.format(Coin.parseCoin("0.000001"))); + // even though last group is 3, that would result in fractional satoshis, which we don't do + assertEquals("0.00010", usMilli.format(Coin.valueOf(10))); + assertEquals("0.00001", usMilli.format(Coin.valueOf(1))); + + BtcFormat usMicro = BtcFormat.getInstance(6, Locale.US, 1, 2, 3); + assertEquals("0.1", usMicro.format(Coin.valueOf(10))); + // even though second group is 2, that would result in fractional satoshis, which we don't do + assertEquals("0.01", usMicro.format(Coin.valueOf(1))); + } + + + /* These just make sure factory methods don't raise exceptions. + * Other tests inspect their return values. */ + @Test + public void factoryTest() { + BtcFormat coded = BtcFormat.getInstance(0, 1, 2, 3); + BtcFormat.getInstance(BtcAutoFormat.Style.CODE); + BtcAutoFormat symbolic = (BtcAutoFormat)BtcFormat.getInstance(BtcAutoFormat.Style.SYMBOL); + assertEquals(2, symbolic.fractionPlaces()); + BtcFormat.getInstance(BtcAutoFormat.Style.CODE, 3); + assertEquals(3, ((BtcAutoFormat)BtcFormat.getInstance(BtcAutoFormat.Style.SYMBOL, 3)).fractionPlaces()); + BtcFormat.getInstance(BtcAutoFormat.Style.SYMBOL, Locale.US, 3); + BtcFormat.getInstance(BtcAutoFormat.Style.CODE, Locale.US); + BtcFormat.getInstance(BtcAutoFormat.Style.SYMBOL, Locale.US); + BtcFormat.getCoinInstance(2, BtcFixedFormat.REPEATING_PLACES); + BtcFormat.getMilliInstance(1, 2, 3); + BtcFormat.getInstance(2); + BtcFormat.getInstance(2, Locale.US); + BtcFormat.getCodeInstance(3); + BtcFormat.getSymbolInstance(3); + BtcFormat.getCodeInstance(Locale.US, 3); + BtcFormat.getSymbolInstance(Locale.US, 3); + try { + BtcFormat.getInstance(SMALLEST_UNIT_EXPONENT + 1); + fail("should not have constructed an instance with denomination less than satoshi"); + } catch (IllegalArgumentException e) {} + } + @Test + public void factoryArgumentsTest() { + Locale locale; + if (Locale.getDefault().equals(GERMANY)) locale = FRANCE; + else locale = GERMANY; + assertEquals(BtcFormat.getInstance(), BtcFormat.getCodeInstance()); + assertEquals(BtcFormat.getInstance(locale), BtcFormat.getCodeInstance(locale)); + assertEquals(BtcFormat.getInstance(BtcAutoFormat.Style.CODE), BtcFormat.getCodeInstance()); + assertEquals(BtcFormat.getInstance(BtcAutoFormat.Style.SYMBOL), BtcFormat.getSymbolInstance()); + assertEquals(BtcFormat.getInstance(BtcAutoFormat.Style.CODE,3), BtcFormat.getCodeInstance(3)); + assertEquals(BtcFormat.getInstance(BtcAutoFormat.Style.SYMBOL,3), BtcFormat.getSymbolInstance(3)); + assertEquals(BtcFormat.getInstance(BtcAutoFormat.Style.CODE,locale), BtcFormat.getCodeInstance(locale)); + assertEquals(BtcFormat.getInstance(BtcAutoFormat.Style.SYMBOL,locale), BtcFormat.getSymbolInstance(locale)); + assertEquals(BtcFormat.getInstance(BtcAutoFormat.Style.CODE,locale,3), BtcFormat.getCodeInstance(locale,3)); + assertEquals(BtcFormat.getInstance(BtcAutoFormat.Style.SYMBOL,locale,3), BtcFormat.getSymbolInstance(locale,3)); + assertEquals(BtcFormat.getCoinInstance(), BtcFormat.getInstance(0)); + assertEquals(BtcFormat.getMilliInstance(), BtcFormat.getInstance(3)); + assertEquals(BtcFormat.getMicroInstance(), BtcFormat.getInstance(6)); + assertEquals(BtcFormat.getCoinInstance(3), BtcFormat.getInstance(0,3)); + assertEquals(BtcFormat.getMilliInstance(3), BtcFormat.getInstance(3,3)); + assertEquals(BtcFormat.getMicroInstance(3), BtcFormat.getInstance(6,3)); + assertEquals(BtcFormat.getCoinInstance(3,4,5), BtcFormat.getInstance(0,3,4,5)); + assertEquals(BtcFormat.getMilliInstance(3,4,5), BtcFormat.getInstance(3,3,4,5)); + assertEquals(BtcFormat.getMicroInstance(3,4,5), BtcFormat.getInstance(6,3,4,5)); + assertEquals(BtcFormat.getCoinInstance(locale), BtcFormat.getInstance(0,locale)); + assertEquals(BtcFormat.getMilliInstance(locale), BtcFormat.getInstance(3,locale)); + assertEquals(BtcFormat.getMicroInstance(locale), BtcFormat.getInstance(6,locale)); + assertEquals(BtcFormat.getCoinInstance(locale,4,5), BtcFormat.getInstance(0,locale,4,5)); + assertEquals(BtcFormat.getMilliInstance(locale,4,5), BtcFormat.getInstance(3,locale,4,5)); + assertEquals(BtcFormat.getMicroInstance(locale,4,5), BtcFormat.getInstance(6,locale,4,5)); + } + + @Test + public void autoDecimalTest() { + BtcFormat codedZero = BtcFormat.getCodeInstance(Locale.US, 0); + BtcFormat symbolZero = BtcFormat.getSymbolInstance(Locale.US, 0); + assertEquals("฿1", symbolZero.format(COIN)); + assertEquals("BTC 1", codedZero.format(COIN)); + assertEquals("µ฿1,000,000", symbolZero.format(COIN.subtract(SATOSHI))); + assertEquals("µBTC 1,000,000", codedZero.format(COIN.subtract(SATOSHI))); + assertEquals("µ฿1,000,000", symbolZero.format(COIN.subtract(Coin.valueOf(50)))); + assertEquals("µBTC 1,000,000", codedZero.format(COIN.subtract(Coin.valueOf(50)))); + assertEquals("µ฿999,999", symbolZero.format(COIN.subtract(Coin.valueOf(51)))); + assertEquals("µBTC 999,999", codedZero.format(COIN.subtract(Coin.valueOf(51)))); + assertEquals("฿1,000", symbolZero.format(COIN.multiply(1000))); + assertEquals("BTC 1,000", codedZero.format(COIN.multiply(1000))); + assertEquals("µ฿1", symbolZero.format(Coin.valueOf(100))); + assertEquals("µBTC 1", codedZero.format(Coin.valueOf(100))); + assertEquals("µ฿1", symbolZero.format(Coin.valueOf(50))); + assertEquals("µBTC 1", codedZero.format(Coin.valueOf(50))); + assertEquals("µ฿0", symbolZero.format(Coin.valueOf(49))); + assertEquals("µBTC 0", codedZero.format(Coin.valueOf(49))); + assertEquals("µ฿0", symbolZero.format(Coin.valueOf(1))); + assertEquals("µBTC 0", codedZero.format(Coin.valueOf(1))); + assertEquals("µ฿500,000", symbolZero.format(Coin.valueOf(49999999))); + assertEquals("µBTC 500,000", codedZero.format(Coin.valueOf(49999999))); + + assertEquals("µ฿499,500", symbolZero.format(Coin.valueOf(49950000))); + assertEquals("µBTC 499,500", codedZero.format(Coin.valueOf(49950000))); + assertEquals("µ฿499,500", symbolZero.format(Coin.valueOf(49949999))); + assertEquals("µBTC 499,500", codedZero.format(Coin.valueOf(49949999))); + assertEquals("µ฿500,490", symbolZero.format(Coin.valueOf(50049000))); + assertEquals("µBTC 500,490", codedZero.format(Coin.valueOf(50049000))); + assertEquals("µ฿500,490", symbolZero.format(Coin.valueOf(50049001))); + assertEquals("µBTC 500,490", codedZero.format(Coin.valueOf(50049001))); + assertEquals("µ฿500,000", symbolZero.format(Coin.valueOf(49999950))); + assertEquals("µBTC 500,000", codedZero.format(Coin.valueOf(49999950))); + assertEquals("µ฿499,999", symbolZero.format(Coin.valueOf(49999949))); + assertEquals("µBTC 499,999", codedZero.format(Coin.valueOf(49999949))); + assertEquals("µ฿500,000", symbolZero.format(Coin.valueOf(50000049))); + assertEquals("µBTC 500,000", codedZero.format(Coin.valueOf(50000049))); + assertEquals("µ฿500,001", symbolZero.format(Coin.valueOf(50000050))); + assertEquals("µBTC 500,001", codedZero.format(Coin.valueOf(50000050))); + + BtcFormat codedTwo = BtcFormat.getCodeInstance(Locale.US, 2); + BtcFormat symbolTwo = BtcFormat.getSymbolInstance(Locale.US, 2); + assertEquals("฿1.00", symbolTwo.format(COIN)); + assertEquals("BTC 1.00", codedTwo.format(COIN)); + assertEquals("µ฿999,999.99", symbolTwo.format(COIN.subtract(SATOSHI))); + assertEquals("µBTC 999,999.99", codedTwo.format(COIN.subtract(SATOSHI))); + assertEquals("฿1,000.00", symbolTwo.format(COIN.multiply(1000))); + assertEquals("BTC 1,000.00", codedTwo.format(COIN.multiply(1000))); + assertEquals("µ฿1.00", symbolTwo.format(Coin.valueOf(100))); + assertEquals("µBTC 1.00", codedTwo.format(Coin.valueOf(100))); + assertEquals("µ฿0.50", symbolTwo.format(Coin.valueOf(50))); + assertEquals("µBTC 0.50", codedTwo.format(Coin.valueOf(50))); + assertEquals("µ฿0.49", symbolTwo.format(Coin.valueOf(49))); + assertEquals("µBTC 0.49", codedTwo.format(Coin.valueOf(49))); + assertEquals("µ฿0.01", symbolTwo.format(Coin.valueOf(1))); + assertEquals("µBTC 0.01", codedTwo.format(Coin.valueOf(1))); + + BtcFormat codedThree = BtcFormat.getCodeInstance(Locale.US, 3); + BtcFormat symbolThree = BtcFormat.getSymbolInstance(Locale.US, 3); + assertEquals("฿1.000", symbolThree.format(COIN)); + assertEquals("BTC 1.000", codedThree.format(COIN)); + assertEquals("µ฿999,999.99", symbolThree.format(COIN.subtract(SATOSHI))); + assertEquals("µBTC 999,999.99", codedThree.format(COIN.subtract(SATOSHI))); + assertEquals("฿1,000.000", symbolThree.format(COIN.multiply(1000))); + assertEquals("BTC 1,000.000", codedThree.format(COIN.multiply(1000))); + assertEquals("₥฿0.001", symbolThree.format(Coin.valueOf(100))); + assertEquals("mBTC 0.001", codedThree.format(Coin.valueOf(100))); + assertEquals("µ฿0.50", symbolThree.format(Coin.valueOf(50))); + assertEquals("µBTC 0.50", codedThree.format(Coin.valueOf(50))); + assertEquals("µ฿0.49", symbolThree.format(Coin.valueOf(49))); + assertEquals("µBTC 0.49", codedThree.format(Coin.valueOf(49))); + assertEquals("µ฿0.01", symbolThree.format(Coin.valueOf(1))); + assertEquals("µBTC 0.01", codedThree.format(Coin.valueOf(1))); + } + + + @Test + public void symbolsCodesTest() { + BtcFixedFormat coin = (BtcFixedFormat)BtcFormat.getCoinInstance(US); + assertEquals("BTC", coin.code()); + assertEquals("฿", coin.symbol()); + BtcFixedFormat cent = (BtcFixedFormat)BtcFormat.getInstance(2, US); + assertEquals("cBTC", cent.code()); + assertEquals("¢฿", cent.symbol()); + BtcFixedFormat milli = (BtcFixedFormat)BtcFormat.getInstance(3, US); + assertEquals("mBTC", milli.code()); + assertEquals("₥฿", milli.symbol()); + BtcFixedFormat micro = (BtcFixedFormat)BtcFormat.getInstance(6, US); + assertEquals("µBTC", micro.code()); + assertEquals("µ฿", micro.symbol()); + BtcFixedFormat deka = (BtcFixedFormat)BtcFormat.getInstance(-1, US); + assertEquals("daBTC", deka.code()); + assertEquals("da฿", deka.symbol()); + BtcFixedFormat hecto = (BtcFixedFormat)BtcFormat.getInstance(-2, US); + assertEquals("hBTC", hecto.code()); + assertEquals("h฿", hecto.symbol()); + BtcFixedFormat kilo = (BtcFixedFormat)BtcFormat.getInstance(-3, US); + assertEquals("kBTC", kilo.code()); + assertEquals("k฿", kilo.symbol()); + BtcFixedFormat mega = (BtcFixedFormat)BtcFormat.getInstance(-6, US); + assertEquals("MBTC", mega.code()); + assertEquals("M฿", mega.symbol()); + BtcFixedFormat noSymbol = (BtcFixedFormat)BtcFormat.getInstance(4, US); + try { + noSymbol.symbol(); + fail("non-standard denomination has no symbol()"); + } catch (IllegalStateException e) {} + try { + noSymbol.code(); + fail("non-standard denomination has no code()"); + } catch (IllegalStateException e) {} + + BtcFixedFormat symbolCoin = (BtcFixedFormat)BtcFormat.builder().locale(US).scale(0). + symbol("B\u20e6").build(); + assertEquals("BTC", symbolCoin.code()); + assertEquals("B⃦", symbolCoin.symbol()); + BtcFixedFormat symbolCent = (BtcFixedFormat)BtcFormat.builder().locale(US).scale(2). + symbol("B\u20e6").build(); + assertEquals("cBTC", symbolCent.code()); + assertEquals("¢B⃦", symbolCent.symbol()); + BtcFixedFormat symbolMilli = (BtcFixedFormat)BtcFormat.builder().locale(US).scale(3). + symbol("B\u20e6").build(); + assertEquals("mBTC", symbolMilli.code()); + assertEquals("₥B⃦", symbolMilli.symbol()); + BtcFixedFormat symbolMicro = (BtcFixedFormat)BtcFormat.builder().locale(US).scale(6). + symbol("B\u20e6").build(); + assertEquals("µBTC", symbolMicro.code()); + assertEquals("µB⃦", symbolMicro.symbol()); + BtcFixedFormat symbolDeka = (BtcFixedFormat)BtcFormat.builder().locale(US).scale(-1). + symbol("B\u20e6").build(); + assertEquals("daBTC", symbolDeka.code()); + assertEquals("daB⃦", symbolDeka.symbol()); + BtcFixedFormat symbolHecto = (BtcFixedFormat)BtcFormat.builder().locale(US).scale(-2). + symbol("B\u20e6").build(); + assertEquals("hBTC", symbolHecto.code()); + assertEquals("hB⃦", symbolHecto.symbol()); + BtcFixedFormat symbolKilo = (BtcFixedFormat)BtcFormat.builder().locale(US).scale(-3). + symbol("B\u20e6").build(); + assertEquals("kBTC", symbolKilo.code()); + assertEquals("kB⃦", symbolKilo.symbol()); + BtcFixedFormat symbolMega = (BtcFixedFormat)BtcFormat.builder().locale(US).scale(-6). + symbol("B\u20e6").build(); + assertEquals("MBTC", symbolMega.code()); + assertEquals("MB⃦", symbolMega.symbol()); + + BtcFixedFormat codeCoin = (BtcFixedFormat)BtcFormat.builder().locale(US).scale(0). + code("XBT").build(); + assertEquals("XBT", codeCoin.code()); + assertEquals("฿", codeCoin.symbol()); + BtcFixedFormat codeCent = (BtcFixedFormat)BtcFormat.builder().locale(US).scale(2). + code("XBT").build(); + assertEquals("cXBT", codeCent.code()); + assertEquals("¢฿", codeCent.symbol()); + BtcFixedFormat codeMilli = (BtcFixedFormat)BtcFormat.builder().locale(US).scale(3). + code("XBT").build(); + assertEquals("mXBT", codeMilli.code()); + assertEquals("₥฿", codeMilli.symbol()); + BtcFixedFormat codeMicro = (BtcFixedFormat)BtcFormat.builder().locale(US).scale(6). + code("XBT").build(); + assertEquals("µXBT", codeMicro.code()); + assertEquals("µ฿", codeMicro.symbol()); + BtcFixedFormat codeDeka = (BtcFixedFormat)BtcFormat.builder().locale(US).scale(-1). + code("XBT").build(); + assertEquals("daXBT", codeDeka.code()); + assertEquals("da฿", codeDeka.symbol()); + BtcFixedFormat codeHecto = (BtcFixedFormat)BtcFormat.builder().locale(US).scale(-2). + code("XBT").build(); + assertEquals("hXBT", codeHecto.code()); + assertEquals("h฿", codeHecto.symbol()); + BtcFixedFormat codeKilo = (BtcFixedFormat)BtcFormat.builder().locale(US).scale(-3). + code("XBT").build(); + assertEquals("kXBT", codeKilo.code()); + assertEquals("k฿", codeKilo.symbol()); + BtcFixedFormat codeMega = (BtcFixedFormat)BtcFormat.builder().locale(US).scale(-6). + code("XBT").build(); + assertEquals("MXBT", codeMega.code()); + assertEquals("M฿", codeMega.symbol()); + + BtcFixedFormat symbolCodeCoin = (BtcFixedFormat)BtcFormat.builder().locale(US).scale(0). + symbol("B\u20e6").code("XBT").build(); + assertEquals("XBT", symbolCodeCoin.code()); + assertEquals("B⃦", symbolCodeCoin.symbol()); + BtcFixedFormat symbolCodeCent = (BtcFixedFormat)BtcFormat.builder().locale(US).scale(2). + symbol("B\u20e6").code("XBT").build(); + assertEquals("cXBT", symbolCodeCent.code()); + assertEquals("¢B⃦", symbolCodeCent.symbol()); + BtcFixedFormat symbolCodeMilli = (BtcFixedFormat)BtcFormat.builder().locale(US).scale(3). + symbol("B\u20e6").code("XBT").build(); + assertEquals("mXBT", symbolCodeMilli.code()); + assertEquals("₥B⃦", symbolCodeMilli.symbol()); + BtcFixedFormat symbolCodeMicro = (BtcFixedFormat)BtcFormat.builder().locale(US).scale(6). + symbol("B\u20e6").code("XBT").build(); + assertEquals("µXBT", symbolCodeMicro.code()); + assertEquals("µB⃦", symbolCodeMicro.symbol()); + BtcFixedFormat symbolCodeDeka = (BtcFixedFormat)BtcFormat.builder().locale(US).scale(-1). + symbol("B\u20e6").code("XBT").build(); + assertEquals("daXBT", symbolCodeDeka.code()); + assertEquals("daB⃦", symbolCodeDeka.symbol()); + BtcFixedFormat symbolCodeHecto = (BtcFixedFormat)BtcFormat.builder().locale(US).scale(-2). + symbol("B\u20e6").code("XBT").build(); + assertEquals("hXBT", symbolCodeHecto.code()); + assertEquals("hB⃦", symbolCodeHecto.symbol()); + BtcFixedFormat symbolCodeKilo = (BtcFixedFormat)BtcFormat.builder().locale(US).scale(-3). + symbol("B\u20e6").code("XBT").build(); + assertEquals("kXBT", symbolCodeKilo.code()); + assertEquals("kB⃦", symbolCodeKilo.symbol()); + BtcFixedFormat symbolCodeMega = (BtcFixedFormat)BtcFormat.builder().locale(US).scale(-6). + symbol("B\u20e6").code("XBT").build(); + assertEquals("MXBT", symbolCodeMega.code()); + assertEquals("MB⃦", symbolCodeMega.symbol()); + } + + /* copied from CoinFormatTest.java and modified */ + @Test + public void parse() throws Exception { + BtcFormat coin = BtcFormat.getCoinInstance(Locale.US); + assertEquals(Coin.COIN, coin.parseObject("1")); + assertEquals(Coin.COIN, coin.parseObject("1.")); + assertEquals(Coin.COIN, coin.parseObject("1.0")); + assertEquals(Coin.COIN, BtcFormat.getCoinInstance(Locale.GERMANY).parseObject("1,0")); + assertEquals(Coin.COIN, coin.parseObject("01.0000000000")); + // TODO work with express positive sign + // assertEquals(Coin.COIN, coin.parseObject("+1.0")); + assertEquals(Coin.COIN.negate(), coin.parseObject("-1")); + assertEquals(Coin.COIN.negate(), coin.parseObject("-1.0")); + + assertEquals(Coin.CENT, coin.parseObject(".01")); + + BtcFormat milli = BtcFormat.getMilliInstance(Locale.US); + assertEquals(Coin.MILLICOIN, milli.parseObject("1")); + assertEquals(Coin.MILLICOIN, milli.parseObject("1.0")); + assertEquals(Coin.MILLICOIN, milli.parseObject("01.0000000000")); + // TODO work with express positive sign + //assertEquals(Coin.MILLICOIN, milli.parseObject("+1.0")); + assertEquals(Coin.MILLICOIN.negate(), milli.parseObject("-1")); + assertEquals(Coin.MILLICOIN.negate(), milli.parseObject("-1.0")); + + BtcFormat micro = BtcFormat.getMicroInstance(Locale.US); + assertEquals(Coin.MICROCOIN, micro.parseObject("1")); + assertEquals(Coin.MICROCOIN, micro.parseObject("1.0")); + assertEquals(Coin.MICROCOIN, micro.parseObject("01.0000000000")); + // TODO work with express positive sign + // assertEquals(Coin.MICROCOIN, micro.parseObject("+1.0")); + assertEquals(Coin.MICROCOIN.negate(), micro.parseObject("-1")); + assertEquals(Coin.MICROCOIN.negate(), micro.parseObject("-1.0")); + } + + /* Copied (and modified) from CoinFormatTest.java */ + @Test + public void btcRounding() throws Exception { + BtcFormat coinFormat = BtcFormat.getCoinInstance(Locale.US); + assertEquals("0", BtcFormat.getCoinInstance(Locale.US, 0).format(ZERO)); + assertEquals("0", coinFormat.format(ZERO, 0)); + assertEquals("0.00", BtcFormat.getCoinInstance(Locale.US, 2).format(ZERO)); + assertEquals("0.00", coinFormat.format(ZERO, 2)); + + assertEquals("1", BtcFormat.getCoinInstance(Locale.US, 0).format(COIN)); + assertEquals("1", coinFormat.format(COIN, 0)); + assertEquals("1.0", BtcFormat.getCoinInstance(Locale.US, 1).format(COIN)); + assertEquals("1.0", coinFormat.format(COIN, 1)); + assertEquals("1.00", BtcFormat.getCoinInstance(Locale.US, 2, 2).format(COIN)); + assertEquals("1.00", coinFormat.format(COIN, 2, 2)); + assertEquals("1.00", BtcFormat.getCoinInstance(Locale.US, 2, 2, 2).format(COIN)); + assertEquals("1.00", coinFormat.format(COIN, 2, 2, 2)); + assertEquals("1.00", BtcFormat.getCoinInstance(Locale.US, 2, 2, 2, 2).format(COIN)); + assertEquals("1.00", coinFormat.format(COIN, 2, 2, 2, 2)); + assertEquals("1.000", BtcFormat.getCoinInstance(Locale.US, 3).format(COIN)); + assertEquals("1.000", coinFormat.format(COIN, 3)); + assertEquals("1.0000", BtcFormat.getCoinInstance(US, 4).format(COIN)); + assertEquals("1.0000", coinFormat.format(COIN, 4)); + + final Coin justNot = COIN.subtract(SATOSHI); + assertEquals("1", BtcFormat.getCoinInstance(US, 0).format(justNot)); + assertEquals("1", coinFormat.format(justNot, 0)); + assertEquals("1.0", BtcFormat.getCoinInstance(US, 1).format(justNot)); + assertEquals("1.0", coinFormat.format(justNot, 1)); + final Coin justNotUnder = Coin.valueOf(99995000); + assertEquals("1.00", BtcFormat.getCoinInstance(US, 2, 2).format(justNot)); + assertEquals("1.00", coinFormat.format(justNot, 2, 2)); + assertEquals("1.00", BtcFormat.getCoinInstance(US, 2, 2).format(justNotUnder)); + assertEquals("1.00", coinFormat.format(justNotUnder, 2, 2)); + assertEquals("1.00", BtcFormat.getCoinInstance(US, 2, 2, 2).format(justNot)); + assertEquals("1.00", coinFormat.format(justNot, 2, 2, 2)); + assertEquals("0.999950", BtcFormat.getCoinInstance(US, 2, 2, 2).format(justNotUnder)); + assertEquals("0.999950", coinFormat.format(justNotUnder, 2, 2, 2)); + assertEquals("0.99999999", BtcFormat.getCoinInstance(US, 2, 2, 2, 2).format(justNot)); + assertEquals("0.99999999", coinFormat.format(justNot, 2, 2, 2, 2)); + assertEquals("0.99999999", BtcFormat.getCoinInstance(US, 2, REPEATING_DOUBLETS).format(justNot)); + assertEquals("0.99999999", coinFormat.format(justNot, 2, REPEATING_DOUBLETS)); + assertEquals("0.999950", BtcFormat.getCoinInstance(US, 2, 2, 2, 2).format(justNotUnder)); + assertEquals("0.999950", coinFormat.format(justNotUnder, 2, 2, 2, 2)); + assertEquals("0.999950", BtcFormat.getCoinInstance(US, 2, REPEATING_DOUBLETS).format(justNotUnder)); + assertEquals("0.999950", coinFormat.format(justNotUnder, 2, REPEATING_DOUBLETS)); + assertEquals("1.000", BtcFormat.getCoinInstance(US, 3).format(justNot)); + assertEquals("1.000", coinFormat.format(justNot, 3)); + assertEquals("1.0000", BtcFormat.getCoinInstance(US, 4).format(justNot)); + assertEquals("1.0000", coinFormat.format(justNot, 4)); + + final Coin slightlyMore = COIN.add(SATOSHI); + assertEquals("1", BtcFormat.getCoinInstance(US, 0).format(slightlyMore)); + assertEquals("1", coinFormat.format(slightlyMore, 0)); + assertEquals("1.0", BtcFormat.getCoinInstance(US, 1).format(slightlyMore)); + assertEquals("1.0", coinFormat.format(slightlyMore, 1)); + assertEquals("1.00", BtcFormat.getCoinInstance(US, 2, 2).format(slightlyMore)); + assertEquals("1.00", coinFormat.format(slightlyMore, 2, 2)); + assertEquals("1.00", BtcFormat.getCoinInstance(US, 2, 2, 2).format(slightlyMore)); + assertEquals("1.00", coinFormat.format(slightlyMore, 2, 2, 2)); + assertEquals("1.00000001", BtcFormat.getCoinInstance(US, 2, 2, 2, 2).format(slightlyMore)); + assertEquals("1.00000001", coinFormat.format(slightlyMore, 2, 2, 2, 2)); + assertEquals("1.00000001", BtcFormat.getCoinInstance(US, 2, REPEATING_DOUBLETS).format(slightlyMore)); + assertEquals("1.00000001", coinFormat.format(slightlyMore, 2, REPEATING_DOUBLETS)); + assertEquals("1.000", BtcFormat.getCoinInstance(US, 3).format(slightlyMore)); + assertEquals("1.000", coinFormat.format(slightlyMore, 3)); + assertEquals("1.0000", BtcFormat.getCoinInstance(US, 4).format(slightlyMore)); + assertEquals("1.0000", coinFormat.format(slightlyMore, 4)); + + final Coin pivot = COIN.add(SATOSHI.multiply(5)); + assertEquals("1.00000005", BtcFormat.getCoinInstance(US, 8).format(pivot)); + assertEquals("1.00000005", coinFormat.format(pivot, 8)); + assertEquals("1.00000005", BtcFormat.getCoinInstance(US, 7, 1).format(pivot)); + assertEquals("1.00000005", coinFormat.format(pivot, 7, 1)); + assertEquals("1.0000001", BtcFormat.getCoinInstance(US, 7).format(pivot)); + assertEquals("1.0000001", coinFormat.format(pivot, 7)); + + final Coin value = Coin.valueOf(1122334455667788l); + assertEquals("11,223,345", BtcFormat.getCoinInstance(US, 0).format(value)); + assertEquals("11,223,345", coinFormat.format(value, 0)); + assertEquals("11,223,344.6", BtcFormat.getCoinInstance(US, 1).format(value)); + assertEquals("11,223,344.6", coinFormat.format(value, 1)); + assertEquals("11,223,344.5567", BtcFormat.getCoinInstance(US, 2, 2).format(value)); + assertEquals("11,223,344.5567", coinFormat.format(value, 2, 2)); + assertEquals("11,223,344.556678", BtcFormat.getCoinInstance(US, 2, 2, 2).format(value)); + assertEquals("11,223,344.556678", coinFormat.format(value, 2, 2, 2)); + assertEquals("11,223,344.55667788", BtcFormat.getCoinInstance(US, 2, 2, 2, 2).format(value)); + assertEquals("11,223,344.55667788", coinFormat.format(value, 2, 2, 2, 2)); + assertEquals("11,223,344.55667788", BtcFormat.getCoinInstance(US, 2, REPEATING_DOUBLETS).format(value)); + assertEquals("11,223,344.55667788", coinFormat.format(value, 2, REPEATING_DOUBLETS)); + assertEquals("11,223,344.557", BtcFormat.getCoinInstance(US, 3).format(value)); + assertEquals("11,223,344.557", coinFormat.format(value, 3)); + assertEquals("11,223,344.5567", BtcFormat.getCoinInstance(US, 4).format(value)); + assertEquals("11,223,344.5567", coinFormat.format(value, 4)); + + BtcFormat megaFormat = BtcFormat.getInstance(-6, US); + assertEquals("21.00", megaFormat.format(MAX_MONEY)); + assertEquals("21", megaFormat.format(MAX_MONEY, 0)); + assertEquals("11.22334455667788", megaFormat.format(value, 0, REPEATING_DOUBLETS)); + assertEquals("11.223344556677", megaFormat.format(Coin.valueOf(1122334455667700l), 0, REPEATING_DOUBLETS)); + assertEquals("11.22334455667788", megaFormat.format(value, 0, REPEATING_TRIPLETS)); + assertEquals("11.223344556677", megaFormat.format(Coin.valueOf(1122334455667700l), 0, REPEATING_TRIPLETS)); + } + + @Test + public void negativeTest() throws Exception { + assertEquals("-1,00 BTC", BtcFormat.getInstance(FRANCE).format(COIN.multiply(-1))); + assertEquals("BTC -1,00", BtcFormat.getInstance(ITALY).format(COIN.multiply(-1))); + assertEquals("฿ -1,00", BtcFormat.getSymbolInstance(ITALY).format(COIN.multiply(-1))); + assertEquals("BTC -1.00", BtcFormat.getInstance(JAPAN).format(COIN.multiply(-1))); + assertEquals("฿-1.00", BtcFormat.getSymbolInstance(JAPAN).format(COIN.multiply(-1))); + assertEquals("(BTC 1.00)", BtcFormat.getInstance(US).format(COIN.multiply(-1))); + assertEquals("(฿1.00)", BtcFormat.getSymbolInstance(US).format(COIN.multiply(-1))); + assertEquals("BTC -१.००", BtcFormat.getInstance(Locale.forLanguageTag("hi-IN")).format(COIN.multiply(-1))); + assertEquals("BTC -๑.๐๐", BtcFormat.getInstance(new Locale("th","TH","TH")).format(COIN.multiply(-1))); + assertEquals("Ƀ-๑.๐๐", BtcFormat.getSymbolInstance(new Locale("th","TH","TH")).format(COIN.multiply(-1))); + } + + /* Warning: these tests assume the state of Locale data extant on the platform on which + * they were written: openjdk 7u21-2.3.9-5 */ + @Test + public void equalityTest() throws Exception { + // First, autodenominator + assertEquals(BtcFormat.getInstance(), BtcFormat.getInstance()); + assertEquals(BtcFormat.getInstance().hashCode(), BtcFormat.getInstance().hashCode()); + + assertNotEquals(BtcFormat.getCodeInstance(), BtcFormat.getSymbolInstance()); + assertNotEquals(BtcFormat.getCodeInstance().hashCode(), BtcFormat.getSymbolInstance().hashCode()); + + assertEquals(BtcFormat.getSymbolInstance(5), BtcFormat.getSymbolInstance(5)); + assertEquals(BtcFormat.getSymbolInstance(5).hashCode(), BtcFormat.getSymbolInstance(5).hashCode()); + + assertNotEquals(BtcFormat.getSymbolInstance(5), BtcFormat.getSymbolInstance(4)); + assertNotEquals(BtcFormat.getSymbolInstance(5).hashCode(), BtcFormat.getSymbolInstance(4).hashCode()); + + /* The underlying formatter is mutable, and its currency code + * and symbol may be reset each time a number is + * formatted or parsed. Here we check to make sure that state is + * ignored when comparing for equality */ + // when formatting + BtcAutoFormat a = (BtcAutoFormat)BtcFormat.getSymbolInstance(US); + BtcAutoFormat b = (BtcAutoFormat)BtcFormat.getSymbolInstance(US); + assertEquals(a, b); + assertEquals(a.hashCode(), b.hashCode()); + a.format(COIN.multiply(1000000)); + assertEquals(a, b); + assertEquals(a.hashCode(), b.hashCode()); + b.format(COIN.divide(1000000)); + assertEquals(a, b); + assertEquals(a.hashCode(), b.hashCode()); + // when parsing + a = (BtcAutoFormat)BtcFormat.getSymbolInstance(US); + b = (BtcAutoFormat)BtcFormat.getSymbolInstance(US); + assertEquals(a, b); + assertEquals(a.hashCode(), b.hashCode()); + a.parseObject("mBTC2"); + assertEquals(a, b); + assertEquals(a.hashCode(), b.hashCode()); + b.parseObject("µ฿4.35"); + assertEquals(a, b); + assertEquals(a.hashCode(), b.hashCode()); + + // FRANCE and GERMANY have different pattterns + assertNotEquals(BtcFormat.getInstance(FRANCE).hashCode(), BtcFormat.getInstance(GERMANY).hashCode()); + // TAIWAN and CHINA differ only in the Locale and Currency, i.e. the patterns and symbols are + // all the same (after setting the currency symbols to bitcoins) + assertNotEquals(BtcFormat.getInstance(TAIWAN), BtcFormat.getInstance(CHINA)); + // but they hash the same because of the DecimalFormatSymbols.hashCode() implementation + + assertEquals(BtcFormat.getSymbolInstance(4), BtcFormat.getSymbolInstance(4)); + assertEquals(BtcFormat.getSymbolInstance(4).hashCode(), BtcFormat.getSymbolInstance(4).hashCode()); + + assertNotEquals(BtcFormat.getSymbolInstance(4), BtcFormat.getSymbolInstance(5)); + assertNotEquals(BtcFormat.getSymbolInstance(4).hashCode(), BtcFormat.getSymbolInstance(5).hashCode()); + + // Fixed-denomination + assertEquals(BtcFormat.getCoinInstance(), BtcFormat.getCoinInstance()); + assertEquals(BtcFormat.getCoinInstance().hashCode(), BtcFormat.getCoinInstance().hashCode()); + + assertEquals(BtcFormat.getMilliInstance(), BtcFormat.getMilliInstance()); + assertEquals(BtcFormat.getMilliInstance().hashCode(), BtcFormat.getMilliInstance().hashCode()); + + assertEquals(BtcFormat.getMicroInstance(), BtcFormat.getMicroInstance()); + assertEquals(BtcFormat.getMicroInstance().hashCode(), BtcFormat.getMicroInstance().hashCode()); + + assertEquals(BtcFormat.getInstance(-6), BtcFormat.getInstance(-6)); + assertEquals(BtcFormat.getInstance(-6).hashCode(), BtcFormat.getInstance(-6).hashCode()); + + assertNotEquals(BtcFormat.getCoinInstance(), BtcFormat.getMilliInstance()); + assertNotEquals(BtcFormat.getCoinInstance().hashCode(), BtcFormat.getMilliInstance().hashCode()); + + assertNotEquals(BtcFormat.getCoinInstance(), BtcFormat.getMicroInstance()); + assertNotEquals(BtcFormat.getCoinInstance().hashCode(), BtcFormat.getMicroInstance().hashCode()); + + assertNotEquals(BtcFormat.getMilliInstance(), BtcFormat.getMicroInstance()); + assertNotEquals(BtcFormat.getMilliInstance().hashCode(), BtcFormat.getMicroInstance().hashCode()); + + assertNotEquals(BtcFormat.getInstance(SMALLEST_UNIT_EXPONENT), + BtcFormat.getInstance(SMALLEST_UNIT_EXPONENT - 1)); + assertNotEquals(BtcFormat.getInstance(SMALLEST_UNIT_EXPONENT).hashCode(), + BtcFormat.getInstance(SMALLEST_UNIT_EXPONENT - 1).hashCode()); + + assertNotEquals(BtcFormat.getCoinInstance(TAIWAN), BtcFormat.getCoinInstance(CHINA)); + + assertNotEquals(BtcFormat.getCoinInstance(2,3), BtcFormat.getCoinInstance(2,4)); + assertNotEquals(BtcFormat.getCoinInstance(2,3).hashCode(), BtcFormat.getCoinInstance(2,4).hashCode()); + + assertNotEquals(BtcFormat.getCoinInstance(2,3), BtcFormat.getCoinInstance(2,3,3)); + assertNotEquals(BtcFormat.getCoinInstance(2,3).hashCode(), BtcFormat.getCoinInstance(2,3,3).hashCode()); + + + } + + @Test + public void attributeTest() throws Exception { + String codePat = BtcFormat.getCodeInstance(Locale.US).pattern(); + assertTrue(codePat.contains("BTC") && ! codePat.contains("(^|[^฿])฿([^฿]|$)") && ! codePat.contains("(^|[^¤])¤([^¤]|$)")); + String symPat = BtcFormat.getSymbolInstance(Locale.US).pattern(); + assertTrue(symPat.contains("฿") && !symPat.contains("BTC") && !symPat.contains("¤¤")); + + assertEquals("BTC #,##0.00;(BTC #,##0.00)", BtcFormat.getCodeInstance(Locale.US).pattern()); + assertEquals("฿#,##0.00;(฿#,##0.00)", BtcFormat.getSymbolInstance(Locale.US).pattern()); + assertEquals('0', BtcFormat.getInstance(Locale.US).symbols().getZeroDigit()); + assertEquals('०', BtcFormat.getInstance(Locale.forLanguageTag("hi-IN")).symbols().getZeroDigit()); + // TODO will this next line work with other JREs? + assertEquals('๐', BtcFormat.getInstance(new Locale("th","TH","TH")).symbols().getZeroDigit()); + } + + @Test + public void toStringTest() { + assertEquals("Auto-format ฿#,##0.00;(฿#,##0.00)", BtcFormat.getSymbolInstance(Locale.US).toString()); + assertEquals("Auto-format ฿#,##0.0000;(฿#,##0.0000)", BtcFormat.getSymbolInstance(Locale.US, 4).toString()); + assertEquals("Auto-format BTC #,##0.00;(BTC #,##0.00)", BtcFormat.getCodeInstance(Locale.US).toString()); + assertEquals("Auto-format BTC #,##0.0000;(BTC #,##0.0000)", BtcFormat.getCodeInstance(Locale.US, 4).toString()); + assertEquals("Coin-format #,##0.00", BtcFormat.getCoinInstance(Locale.US).toString()); + assertEquals("Millicoin-format #,##0.00", BtcFormat.getMilliInstance(Locale.US).toString()); + assertEquals("Microcoin-format #,##0.00", BtcFormat.getMicroInstance(Locale.US).toString()); + assertEquals("Coin-format #,##0.000", BtcFormat.getCoinInstance(Locale.US,3).toString()); + assertEquals("Coin-format #,##0.000(####)(#######)", BtcFormat.getCoinInstance(Locale.US,3,4,7).toString()); + assertEquals("Kilocoin-format #,##0.000", BtcFormat.getInstance(-3,Locale.US,3).toString()); + assertEquals("Kilocoin-format #,##0.000(####)(#######)", BtcFormat.getInstance(-3,Locale.US,3,4,7).toString()); + assertEquals("Decicoin-format #,##0.000", BtcFormat.getInstance(1,Locale.US,3).toString()); + assertEquals("Decicoin-format #,##0.000(####)(#######)", BtcFormat.getInstance(1,Locale.US,3,4,7).toString()); + assertEquals("Dekacoin-format #,##0.000", BtcFormat.getInstance(-1,Locale.US,3).toString()); + assertEquals("Dekacoin-format #,##0.000(####)(#######)", BtcFormat.getInstance(-1,Locale.US,3,4,7).toString()); + assertEquals("Hectocoin-format #,##0.000", BtcFormat.getInstance(-2,Locale.US,3).toString()); + assertEquals("Hectocoin-format #,##0.000(####)(#######)", BtcFormat.getInstance(-2,Locale.US,3,4,7).toString()); + assertEquals("Megacoin-format #,##0.000", BtcFormat.getInstance(-6,Locale.US,3).toString()); + assertEquals("Megacoin-format #,##0.000(####)(#######)", BtcFormat.getInstance(-6,Locale.US,3,4,7).toString()); + assertEquals("Fixed (-4) format #,##0.000", BtcFormat.getInstance(-4,Locale.US,3).toString()); + assertEquals("Fixed (-4) format #,##0.000(####)", BtcFormat.getInstance(-4,Locale.US,3,4).toString()); + assertEquals("Fixed (-4) format #,##0.000(####)(#######)", + BtcFormat.getInstance(-4, Locale.US, 3, 4, 7).toString()); + + assertEquals("Auto-format ฿#,##0.00;(฿#,##0.00)", + BtcFormat.builder().style(SYMBOL).code("USD").locale(US).build().toString()); + assertEquals("Auto-format #.##0,00 $", + BtcFormat.builder().style(SYMBOL).symbol("$").locale(GERMANY).build().toString()); + assertEquals("Auto-format #.##0,0000 $", + BtcFormat.builder().style(SYMBOL).symbol("$").fractionDigits(4).locale(GERMANY).build().toString()); + assertEquals("Auto-format BTC#,00฿;BTC-#,00฿", + BtcFormat.builder().style(SYMBOL).locale(GERMANY).pattern("¤¤#¤").build().toString()); + assertEquals("Coin-format BTC#,00฿;BTC-#,00฿", + BtcFormat.builder().scale(0).locale(GERMANY).pattern("¤¤#¤").build().toString()); + assertEquals("Millicoin-format BTC#.00฿;BTC-#.00฿", + BtcFormat.builder().scale(3).locale(US).pattern("¤¤#¤").build().toString()); + } + + @Test + public void patternDecimalPlaces() { + /* The pattern format provided by DecimalFormat includes specification of fractional digits, + * but we ignore that because we have alternative mechanism for specifying that.. */ + BtcFormat f = BtcFormat.builder().locale(US).scale(3).pattern("¤¤ #.0").fractionDigits(3).build(); + assertEquals("Millicoin-format BTC #.000;BTC -#.000", f.toString()); + assertEquals("mBTC 1000.000", f.format(COIN)); + } + + @Test + public void builderTest() { + Locale locale; + if (Locale.getDefault().equals(GERMANY)) locale = FRANCE; + else locale = GERMANY; + + assertEquals(BtcFormat.builder().build(), BtcFormat.getCoinInstance()); + try { + BtcFormat.builder().scale(0).style(CODE); + fail("Invoking both scale() and style() on a Builder should raise exception"); + } catch (IllegalStateException e) {} + try { + BtcFormat.builder().style(CODE).scale(0); + fail("Invoking both style() and scale() on a Builder should raise exception"); + } catch (IllegalStateException e) {} + + BtcFormat built = BtcFormat.builder().style(BtcAutoFormat.Style.CODE).fractionDigits(4).build(); + assertEquals(built, BtcFormat.getCodeInstance(4)); + built = BtcFormat.builder().style(BtcAutoFormat.Style.SYMBOL).fractionDigits(4).build(); + assertEquals(built, BtcFormat.getSymbolInstance(4)); + + built = BtcFormat.builder().scale(0).build(); + assertEquals(built, BtcFormat.getCoinInstance()); + built = BtcFormat.builder().scale(3).build(); + assertEquals(built, BtcFormat.getMilliInstance()); + built = BtcFormat.builder().scale(6).build(); + assertEquals(built, BtcFormat.getMicroInstance()); + + built = BtcFormat.builder().locale(locale).scale(0).build(); + assertEquals(built, BtcFormat.getCoinInstance(locale)); + built = BtcFormat.builder().locale(locale).scale(3).build(); + assertEquals(built, BtcFormat.getMilliInstance(locale)); + built = BtcFormat.builder().locale(locale).scale(6).build(); + assertEquals(built, BtcFormat.getMicroInstance(locale)); + + built = BtcFormat.builder().minimumFractionDigits(3).scale(0).build(); + assertEquals(built, BtcFormat.getCoinInstance(3)); + built = BtcFormat.builder().minimumFractionDigits(3).scale(3).build(); + assertEquals(built, BtcFormat.getMilliInstance(3)); + built = BtcFormat.builder().minimumFractionDigits(3).scale(6).build(); + assertEquals(built, BtcFormat.getMicroInstance(3)); + + built = BtcFormat.builder().fractionGroups(3,4).scale(0).build(); + assertEquals(built, BtcFormat.getCoinInstance(2,3,4)); + built = BtcFormat.builder().fractionGroups(3,4).scale(3).build(); + assertEquals(built, BtcFormat.getMilliInstance(2,3,4)); + built = BtcFormat.builder().fractionGroups(3,4).scale(6).build(); + assertEquals(built, BtcFormat.getMicroInstance(2,3,4)); + + built = BtcFormat.builder().pattern("#,####.#").scale(6).locale(GERMANY).build(); + assertEquals("100.0000,00", built.format(COIN)); + built = BtcFormat.builder().pattern("#,####.#").scale(6).locale(GERMANY).build(); + assertEquals("-100.0000,00", built.format(COIN.multiply(-1))); + built = BtcFormat.builder().localizedPattern("#.####,#").scale(6).locale(GERMANY).build(); + assertEquals("100.0000,00", built.format(COIN)); + + built = BtcFormat.builder().pattern("¤#,####.#").style(CODE).locale(GERMANY).build(); + assertEquals("฿-1,00", built.format(COIN.multiply(-1))); + built = BtcFormat.builder().pattern("¤¤ #,####.#").style(SYMBOL).locale(GERMANY).build(); + assertEquals("BTC -1,00", built.format(COIN.multiply(-1))); + built = BtcFormat.builder().pattern("¤¤##,###.#").scale(3).locale(US).build(); + assertEquals("mBTC1,000.00", built.format(COIN)); + built = BtcFormat.builder().pattern("¤ ##,###.#").scale(3).locale(US).build(); + assertEquals("₥฿ 1,000.00", built.format(COIN)); + + try { + BtcFormat.builder().pattern("¤¤##,###.#").scale(4).locale(US).build().format(COIN); + fail("Pattern with currency sign and non-standard denomination should raise exception"); + } catch (IllegalStateException e) {} + + try { + BtcFormat.builder().localizedPattern("¤¤##,###.#").scale(4).locale(US).build().format(COIN); + fail("Localized pattern with currency sign and non-standard denomination should raise exception"); + } catch (IllegalStateException e) {} + + built = BtcFormat.builder().style(SYMBOL).symbol("B\u20e6").locale(US).build(); + assertEquals("B⃦1.00", built.format(COIN)); + built = BtcFormat.builder().style(CODE).code("XBT").locale(US).build(); + assertEquals("XBT 1.00", built.format(COIN)); + built = BtcFormat.builder().style(SYMBOL).symbol("$").locale(GERMANY).build(); + assertEquals("1,00 $", built.format(COIN)); + // Setting the currency code on a DecimalFormatSymbols object can affect the currency symbol. + built = BtcFormat.builder().style(SYMBOL).code("USD").locale(US).build(); + assertEquals("฿1.00", built.format(COIN)); + + built = BtcFormat.builder().style(SYMBOL).symbol("B\u20e6").locale(US).build(); + assertEquals("₥B⃦1.00", built.format(COIN.divide(1000))); + built = BtcFormat.builder().style(CODE).code("XBT").locale(US).build(); + assertEquals("mXBT 1.00", built.format(COIN.divide(1000))); + + built = BtcFormat.builder().style(SYMBOL).symbol("B\u20e6").locale(US).build(); + assertEquals("µB⃦1.00", built.format(valueOf(100))); + built = BtcFormat.builder().style(CODE).code("XBT").locale(US).build(); + assertEquals("µXBT 1.00", built.format(valueOf(100))); + + /* The prefix of a pattern can have number symbols in quotes. + * Make sure our custom negative-subpattern creator handles this. */ + built = BtcFormat.builder().pattern("'#'¤#0").scale(0).locale(US).build(); + assertEquals("#฿-1.00", built.format(COIN.multiply(-1))); + built = BtcFormat.builder().pattern("'#0'¤#0").scale(0).locale(US).build(); + assertEquals("#0฿-1.00", built.format(COIN.multiply(-1))); + // this is an escaped quote between two hash marks in one set of quotes, not + // two adjacent quote-enclosed hash-marks: + built = BtcFormat.builder().pattern("'#''#'¤#0").scale(0).locale(US).build(); + assertEquals("#'#฿-1.00", built.format(COIN.multiply(-1))); + built = BtcFormat.builder().pattern("'#0''#'¤#0").scale(0).locale(US).build(); + assertEquals("#0'#฿-1.00", built.format(COIN.multiply(-1))); + built = BtcFormat.builder().pattern("'#0#'¤#0").scale(0).locale(US).build(); + assertEquals("#0#฿-1.00", built.format(COIN.multiply(-1))); + built = BtcFormat.builder().pattern("'#0'E'#'¤#0").scale(0).locale(US).build(); + assertEquals("#0E#฿-1.00", built.format(COIN.multiply(-1))); + built = BtcFormat.builder().pattern("E'#0''#'¤#0").scale(0).locale(US).build(); + assertEquals("E#0'#฿-1.00", built.format(COIN.multiply(-1))); + built = BtcFormat.builder().pattern("E'#0#'¤#0").scale(0).locale(US).build(); + assertEquals("E#0#฿-1.00", built.format(COIN.multiply(-1))); + built = BtcFormat.builder().pattern("E'#0''''#'¤#0").scale(0).locale(US).build(); + assertEquals("E#0''#฿-1.00", built.format(COIN.multiply(-1))); + built = BtcFormat.builder().pattern("''#0").scale(0).locale(US).build(); + assertEquals("'-1.00", built.format(COIN.multiply(-1))); + + // immutability check for fixed-denomination formatters, w/ & w/o custom pattern + BtcFormat a = BtcFormat.builder().scale(3).build(); + BtcFormat b = BtcFormat.builder().scale(3).build(); + assertEquals(a, b); + assertEquals(a.hashCode(), b.hashCode()); + a.format(COIN.multiply(1000000)); + assertEquals(a, b); + assertEquals(a.hashCode(), b.hashCode()); + b.format(COIN.divide(1000000)); + assertEquals(a, b); + assertEquals(a.hashCode(), b.hashCode()); + a = BtcFormat.builder().scale(3).pattern("¤#.#").build(); + b = BtcFormat.builder().scale(3).pattern("¤#.#").build(); + assertEquals(a, b); + assertEquals(a.hashCode(), b.hashCode()); + a.format(COIN.multiply(1000000)); + assertEquals(a, b); + assertEquals(a.hashCode(), b.hashCode()); + b.format(COIN.divide(1000000)); + assertEquals(a, b); + assertEquals(a.hashCode(), b.hashCode()); + + } + +}