com.google.gwt.i18n.client.NumberFormat Maven / Gradle / Ivy
Show all versions of vaadin-client Show documentation
/*
* Copyright 2008 Google Inc.
*
* 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.gwt.i18n.client;
import com.google.gwt.i18n.client.constants.NumberConstants;
import java.math.BigDecimal;
import java.math.BigInteger;
/**
* Formats and parses numbers using locale-sensitive patterns.
*
* This class provides comprehensive and flexible support for a wide variety of
* localized formats, including
*
* - Locale-specific symbols such as decimal point, group separator,
* digit representation, currency symbol, percent, and permill
* - Numeric variations including integers ("123"), fixed-point
* numbers ("123.4"), scientific notation ("1.23E4"), percentages ("12%"), and
* currency amounts ("$123")
* - Predefined standard patterns that can be used both for parsing
* and formatting, including {@link #getDecimalFormat() decimal},
* {@link #getCurrencyFormat() currency},
* {@link #getPercentFormat() percentages}, and
* {@link #getScientificFormat() scientific}
* - Custom patterns and supporting features designed to make it
* possible to parse and format numbers in any locale, including support for
* Western, Arabic, and Indic digits
*
*
* Patterns
*
* Formatting and parsing are based on customizable patterns that can include a
* combination of literal characters and special characters that act as
* placeholders and are replaced by their localized counterparts. Many
* characters in a pattern are taken literally; they are matched during parsing
* and output unchanged during formatting. Special characters, on the other
* hand, stand for other characters, strings, or classes of characters. For
* example, the '#
' character is replaced by a localized digit.
*
*
*
* Often the replacement character is the same as the pattern character. In the
* U.S. locale, for example, the ',
' grouping character is
* replaced by the same character ',
'. However, the replacement
* is still actually happening, and in a different locale, the grouping
* character may change to a different character, such as '.
'.
* Some special characters affect the behavior of the formatter by their
* presence. For example, if the percent character is seen, then the value is
* multiplied by 100 before being displayed.
*
*
*
* The characters listed below are used in patterns. Localized symbols use the
* corresponding characters taken from corresponding locale symbol collection,
* which can be found in the properties files residing in the
* com.google.gwt.i18n.client.constants
. To insert
* a special character in a pattern as a literal (that is, without any special
* meaning) the character must be quoted. There are some exceptions to this
* which are noted below.
*
*
*
*
* Symbol
* Location
* Localized?
* Meaning
*
*
*
* 0
* Number
* Yes
* Digit
*
*
*
* #
* Number
* Yes
* Digit, zero shows as absent
*
*
*
* .
* Number
* Yes
* Decimal separator or monetary decimal separator
*
*
*
* -
* Number
* Yes
* Minus sign
*
*
*
* ,
* Number
* Yes
* Grouping separator
*
*
*
* E
* Number
* Yes
* Separates mantissa and exponent in scientific notation; need not be
* quoted in prefix or suffix
*
*
*
* ;
* Subpattern boundary
* Yes
* Separates positive and negative subpatterns
*
*
*
* %
* Prefix or suffix
* Yes
* Multiply by 100 and show as percentage
*
*
*
* \u2030
(\u005Cu2030)
* Prefix or suffix
* Yes
* Multiply by 1000 and show as per mille
*
*
*
* \u00A4
(\u005Cu00A4)
* Prefix or suffix
* No
* Currency sign, replaced by currency symbol; if doubled, replaced by
* international currency symbol; if present in a pattern, the monetary decimal
* separator is used instead of the decimal separator
*
*
*
* '
* Prefix or suffix
* No
* Used to quote special characters in a prefix or suffix; for example,
* "'#'#"
formats 123
to "#123"
;
* to create a single quote itself, use two in succession, such as
* "# o''clock"
*
*
*
*
*
* A NumberFormat
pattern contains a postive and negative
* subpattern separated by a semicolon, such as
* "#,##0.00;(#,##0.00)"
. Each subpattern has a prefix, a
* numeric part, and a suffix. If there is no explicit negative subpattern, the
* negative subpattern is the localized minus sign prefixed to the positive
* subpattern. That is, "0.00"
alone is equivalent to
* "0.00;-0.00"
. If there is an explicit negative subpattern, it
* serves only to specify the negative prefix and suffix; the number of digits,
* minimal digits, and other characteristics are ignored in the negative
* subpattern. That means that "#,##0.0#;(#)"
has precisely the
* same result as "#,##0.0#;(#,##0.0#)"
.
*
*
*
* The prefixes, suffixes, and various symbols used for infinity, digits,
* thousands separators, decimal separators, etc. may be set to arbitrary
* values, and they will appear properly during formatting. However, care must
* be taken that the symbols and strings do not conflict, or parsing will be
* unreliable. For example, the decimal separator and thousands separator should
* be distinct characters, or parsing will be impossible.
*
*
*
* The grouping separator is a character that separates clusters of integer
* digits to make large numbers more legible. It commonly used for thousands,
* but in some locales it separates ten-thousands. The grouping size is the
* number of digits between the grouping separators, such as 3 for "100,000,000"
* or 4 for "1 0000 0000".
*
*
* Pattern Grammar (BNF)
*
* The pattern itself uses the following grammar:
*
*
*
*
* pattern
* :=
* subpattern (';
'
* subpattern)?
*
*
* subpattern
* :=
* prefix? number exponent? suffix?
*
*
* number
* :=
* (integer ('.
' fraction)?) |
* sigDigits
*
*
* prefix
* :=
* '\u005Cu0000
'..'\u005CuFFFD
' -
* specialCharacters
*
*
* suffix
* :=
* '\u005Cu0000
'..'\u005CuFFFD
' -
* specialCharacters
*
*
* integer
* :=
* '#
'* '0
'*'0
'
*
*
* fraction
* :=
* '0
'* '#
'*
*
*
* sigDigits
* :=
* '#
'* '@
''@
'* '#
'*
*
*
* exponent
* :=
* 'E
' '+
'? '0
'* '0
'
*
*
* padSpec
* :=
* '*
' padChar
*
*
* padChar
* :=
* '\u005Cu0000
'..'\u005CuFFFD
' - quote
*
*
*
*
* Notation:
*
*
*
*
* X*
* 0 or more instances of X
*
*
*
* X?
* 0 or 1 instances of X
*
*
*
* X|Y
* either X or Y
*
*
*
* C..D
* any character from C up to D, inclusive
*
*
*
* S-T
* characters in S, except those in T
*
*
*
*
* The first subpattern is for positive numbers. The second (optional)
* subpattern is for negative numbers.
*
*
* Example
{@example com.google.gwt.examples.NumberFormatExample}
*
*
*/
public class NumberFormat {
// Sets of constants as defined for the current locale from CLDR.
protected static final NumberConstants localizedNumberConstants = LocaleInfo.getCurrentLocale().getNumberConstants();
/**
* Current NumberConstants interface to use, see
* {@link #setForcedLatinDigits(boolean)} for changing it.
*/
protected static NumberConstants defaultNumberConstants = localizedNumberConstants;
// Cached instances of standard formatters.
private static NumberFormat cachedCurrencyFormat;
private static NumberFormat cachedDecimalFormat;
private static NumberFormat cachedPercentFormat;
private static NumberFormat cachedScientificFormat;
// Constants for characters used in programmatic (unlocalized) patterns.
private static final char CURRENCY_SIGN = '\u00A4';
// Number constants mapped to use latin digits/separators.
private static NumberConstants latinNumberConstants = null;
// Localized characters for dot and comma in number patterns, used to produce
// the latin mapping for arbitrary locales. Any separator not in either of
// these strings will be mapped to non-breaking space (U+00A0).
private static final String LOCALIZED_COMMA_EQUIVALENTS = ",\u060C\u066B\u3001\uFE10\uFE11\uFE50\uFE51\uFF0C\uFF64";
private static final String LOCALIZED_DOT_EQUIVALENTS = ".\u2024\u3002\uFE12\uFE52\uFF0E\uFF61";
private static final char PATTERN_DECIMAL_SEPARATOR = '.';
private static final char PATTERN_DIGIT = '#';
private static final char PATTERN_EXPONENT = 'E';
private static final char PATTERN_GROUPING_SEPARATOR = ',';
private static final char PATTERN_MINUS = '-';
private static final char PATTERN_PER_MILLE = '\u2030';
private static final char PATTERN_PERCENT = '%';
private static final char PATTERN_SEPARATOR = ';';
private static final char PATTERN_ZERO_DIGIT = '0';
private static final char QUOTE = '\'';
/**
* Returns true if all new NumberFormat instances will use latin digits and
* related characters rather than the localized ones.
*/
public static boolean forcedLatinDigits() {
return defaultNumberConstants != localizedNumberConstants;
}
/**
* Provides the standard currency format for the current locale.
*
* @return a NumberFormat
capable of producing and consuming
* currency format for the default locale
*/
public static NumberFormat getCurrencyFormat() {
if (cachedCurrencyFormat == null) {
cachedCurrencyFormat = getCurrencyFormat(CurrencyList.get().getDefault());
}
return cachedCurrencyFormat;
}
/**
* Provides the standard currency format for the current locale using a
* specified currency.
*
* @param currencyData currency data to use
* @return a NumberFormat
capable of producing and consuming
* currency format for the current locale
*/
public static NumberFormat getCurrencyFormat(CurrencyData currencyData) {
return new NumberFormat(defaultNumberConstants.currencyPattern(),
currencyData, false);
}
/**
* Provides the standard currency format for the current locale using a
* specified currency.
*
* @param currencyCode valid currency code, as defined in
* com.google.gwt.i18n.client.constants.CurrencyCodeMapConstants.properties
* @return a NumberFormat
capable of producing and consuming
* currency format for the current locale
* @throws IllegalArgumentException if the currency code is unknown
*/
public static NumberFormat getCurrencyFormat(String currencyCode) {
return getCurrencyFormat(lookupCurrency(currencyCode));
}
/**
* Provides the standard decimal format for the default locale.
*
* @return a NumberFormat
capable of producing and consuming
* decimal format for the default locale
*/
public static NumberFormat getDecimalFormat() {
if (cachedDecimalFormat == null) {
cachedDecimalFormat = new NumberFormat(
defaultNumberConstants.decimalPattern(),
CurrencyList.get().getDefault(), false);
}
return cachedDecimalFormat;
}
/**
* Gets a NumberFormat
instance for the default locale using
* the specified pattern and the default currencyCode.
*
* @param pattern pattern for this formatter
* @return a NumberFormat instance
* @throws IllegalArgumentException if the specified pattern is invalid
*/
public static NumberFormat getFormat(String pattern) {
return new NumberFormat(pattern, CurrencyList.get().getDefault(), true);
}
/**
* Gets a custom NumberFormat
instance for the default locale
* using the specified pattern and currency code.
*
* @param pattern pattern for this formatter
* @param currencyData currency data
* @return a NumberFormat instance
* @throws IllegalArgumentException if the specified pattern is invalid
*/
public static NumberFormat getFormat(String pattern,
CurrencyData currencyData) {
return new NumberFormat(pattern, currencyData, true);
}
/**
* Gets a custom NumberFormat
instance for the default locale
* using the specified pattern and currency code.
*
* @param pattern pattern for this formatter
* @param currencyCode international currency code
* @return a NumberFormat instance
* @throws IllegalArgumentException if the specified pattern is invalid
* or the currency code is unknown
*/
public static NumberFormat getFormat(String pattern, String currencyCode) {
return new NumberFormat(pattern, lookupCurrency(currencyCode), true);
}
/**
* Provides the global currency format for the current locale, using its
* default currency.
*
* @return a NumberFormat
capable of producing and consuming
* currency format for the current locale
*/
public static NumberFormat getGlobalCurrencyFormat() {
return getGlobalCurrencyFormat(CurrencyList.get().getDefault());
}
/**
* Provides the global currency format for the current locale, using a
* specified currency.
*
* @param currencyData currency data to use
* @return a NumberFormat
capable of producing and consuming
* currency format for the current locale
*/
public static NumberFormat getGlobalCurrencyFormat(CurrencyData currencyData) {
return new NumberFormat(defaultNumberConstants.globalCurrencyPattern(),
currencyData, false);
}
/**
* Provides the global currency format for the current locale, using a
* specified currency.
*
* @param currencyCode valid currency code, as defined in
* com.google.gwt.i18n.client.constants.CurrencyCodeMapConstants.properties
* @return a NumberFormat
capable of producing and consuming
* currency format for the current locale
* @throws IllegalArgumentException if the currency code is unknown
*/
public static NumberFormat getGlobalCurrencyFormat(String currencyCode) {
return getGlobalCurrencyFormat(lookupCurrency(currencyCode));
}
/**
* Provides the standard percent format for the default locale.
*
* @return a NumberFormat
capable of producing and consuming
* percent format for the default locale
*/
public static NumberFormat getPercentFormat() {
if (cachedPercentFormat == null) {
cachedPercentFormat = new NumberFormat(
defaultNumberConstants.percentPattern(),
CurrencyList.get().getDefault(), false);
}
return cachedPercentFormat;
}
/**
* Provides the standard scientific format for the default locale.
*
* @return a NumberFormat
capable of producing and consuming
* scientific format for the default locale
*/
public static NumberFormat getScientificFormat() {
if (cachedScientificFormat == null) {
cachedScientificFormat = new NumberFormat(
defaultNumberConstants.scientificPattern(),
CurrencyList.get().getDefault(), false);
}
return cachedScientificFormat;
}
/**
* Provides the simple currency format for the current locale using its
* default currency. Note that these formats may be ambiguous if the
* currency isn't clear from other content on the page.
*
* @return a NumberFormat
capable of producing and consuming
* currency format for the current locale
*/
public static NumberFormat getSimpleCurrencyFormat() {
return getSimpleCurrencyFormat(CurrencyList.get().getDefault());
}
/**
* Provides the simple currency format for the current locale using a
* specified currency. Note that these formats may be ambiguous if the
* currency isn't clear from other content on the page.
*
* @param currencyData currency data to use
* @return a NumberFormat
capable of producing and consuming
* currency format for the current locale
*/
public static NumberFormat getSimpleCurrencyFormat(CurrencyData currencyData) {
return new NumberFormat(defaultNumberConstants.simpleCurrencyPattern(),
currencyData, false);
}
/**
* Provides the simple currency format for the current locale using a
* specified currency. Note that these formats may be ambiguous if the
* currency isn't clear from other content on the page.
*
* @param currencyCode valid currency code, as defined in
* com.google.gwt.i18n.client
* .constants.CurrencyCodeMapConstants.properties
* @return a NumberFormat
capable of producing and consuming
* currency format for the current locale
* @throws IllegalArgumentException if the currency code is unknown
*/
public static NumberFormat getSimpleCurrencyFormat(String currencyCode) {
return getSimpleCurrencyFormat(lookupCurrency(currencyCode));
}
/**
* Specify whether all new NumberFormat instances will use latin digits
* and related characters rather than the localized ones.
*
* @param useLatinDigits true if latin digits/etc should be used, false if
* localized digits/etc should be used.
*/
public static void setForcedLatinDigits(boolean useLatinDigits) {
// Invalidate cached formats if changing
if (useLatinDigits != forcedLatinDigits()) {
cachedCurrencyFormat = null;
cachedDecimalFormat = null;
cachedPercentFormat = null;
cachedScientificFormat = null;
}
if (useLatinDigits) {
if (latinNumberConstants == null) {
latinNumberConstants = createLatinNumberConstants(
localizedNumberConstants);
}
defaultNumberConstants = latinNumberConstants;
} else {
defaultNumberConstants = localizedNumberConstants;
}
}
/**
* Create a delocalized NumberConstants instance from a localized one.
*
* @param orig localized NumberConstants instance
* @return NumberConstants instance using latin digits/etc
*/
protected static NumberConstants createLatinNumberConstants(
final NumberConstants orig) {
final String groupingSeparator = remapSeparator(
orig.groupingSeparator());
final String decimalSeparator = remapSeparator(
orig.decimalSeparator());
final String monetaryGroupingSeparator = remapSeparator(
orig.monetaryGroupingSeparator());
final String monetarySeparator = remapSeparator(
orig.monetarySeparator());
return new NumberConstants() {
@Override
public String currencyPattern() {
return orig.currencyPattern();
}
@Override
public String decimalPattern() {
return orig.decimalPattern();
}
@Override
public String decimalSeparator() {
return decimalSeparator;
}
@Override
public String defCurrencyCode() {
return orig.defCurrencyCode();
}
@Override
public String exponentialSymbol() {
return orig.exponentialSymbol();
}
@Override
public String globalCurrencyPattern() {
return orig.globalCurrencyPattern();
}
@Override
public String groupingSeparator() {
return groupingSeparator;
}
@Override
public String infinity() {
return orig.infinity();
}
@Override
public String minusSign() {
return orig.minusSign();
}
@Override
public String monetaryGroupingSeparator() {
return monetaryGroupingSeparator;
}
@Override
public String monetarySeparator() {
return monetarySeparator;
}
@Override
public String notANumber() {
return orig.notANumber();
}
@Override
public String percent() {
return orig.percent();
}
@Override
public String percentPattern() {
return orig.percentPattern();
}
@Override
public String perMill() {
return orig.perMill();
}
@Override
public String plusSign() {
return orig.plusSign();
}
@Override
public String scientificPattern() {
return orig.scientificPattern();
}
@Override
public String simpleCurrencyPattern() {
return orig.simpleCurrencyPattern();
}
@Override
public String zeroDigit() {
return "0";
}
};
}
/**
* Remap a localized separator to an equivalent latin one.
*
* @param separator
* @return delocalized separator character
*/
protected static String remapSeparator(String separator) {
char ch = separator.length() > 0 ? separator.charAt(0) : 0xFFFF;
if (LOCALIZED_DOT_EQUIVALENTS.indexOf(ch) >= 0) {
return ".";
}
if (LOCALIZED_COMMA_EQUIVALENTS.indexOf(ch) >= 0) {
return ",";
}
return "\u00A0";
}
/**
* Appends a scaled string representation to a buffer, returning the scale
* (which is the number of places to the right of the end of the string the
* decimal point should be moved -- i.e., 3.5 would be added to the buffer
* as "35" and a returned scale of -1).
*
* @param buf
* @param val
* @return scale to apply to the result
*/
// @VisibleForTesting
static int toScaledString(StringBuilder buf, double val) {
int startLen = buf.length();
buf.append(toPrecision(val, 20));
int scale = 0;
// remove exponent if present, adjusting scale
int expIdx = buf.indexOf("e", startLen);
if (expIdx < 0) {
expIdx = buf.indexOf("E", startLen);
}
if (expIdx >= 0) {
int expDigits = expIdx + 1;
if (expDigits < buf.length() && buf.charAt(expDigits) == '+') {
++expDigits;
}
if (expDigits < buf.length()) {
scale = Integer.parseInt(buf.substring(expDigits));
}
buf.delete(expIdx, buf.length());
}
// remove decimal point if present, adjusting scale
int dot = buf.indexOf(".", startLen);
if (dot >= 0) {
buf.deleteCharAt(dot);
scale -= buf.length() - dot;
}
return scale;
}
/**
* Lookup a currency code.
*
* @param currencyCode ISO4217 currency code
* @return a CurrencyData instance
* @throws IllegalArgumentException if the currency code is unknown
*/
private static CurrencyData lookupCurrency(String currencyCode) {
CurrencyData currencyData = CurrencyList.get().lookup(currencyCode);
if (currencyData == null) {
throw new IllegalArgumentException("Currency code " + currencyCode
+ " is unkown in locale "
+ LocaleInfo.getCurrentLocale().getLocaleName());
}
return currencyData;
}
/**
* Convert a double to a string with {@code digits} precision. The resulting
* string may still be in exponential notation.
*
* @param d double value
* @param digits number of digits of precision to include
* @return non-localized string representation of {@code d}
*/
private static native String toPrecision(double d, int digits) /*-{
return d.toPrecision(digits);
}-*/;
/**
* Information about the currency being used.
*/
private CurrencyData currencyData;
/**
* Holds the current decimal position during one call to
* {@link #format(boolean, StringBuilder, int)}.
*/
private transient int decimalPosition;
/**
* Forces the decimal separator to always appear in a formatted number.
*/
private boolean decimalSeparatorAlwaysShown = false;
/**
* Holds the current digits length during one call to
* {@link #format(boolean, StringBuilder, int)}.
*/
private transient int digitsLength;
/**
* Holds the current exponent during one call to
* {@link #format(boolean, StringBuilder, int)}.
*/
private transient int exponent;
/**
* The number of digits between grouping separators in the integer portion of
* a number.
*/
private int groupingSize = 3;
private boolean isCurrencyFormat = false;
private int maximumFractionDigits = 3; // invariant, >= minFractionDigits.
private int maximumIntegerDigits = 40;
private int minExponentDigits;
private int minimumFractionDigits = 0;
private int minimumIntegerDigits = 1;
// The multiplier for use in percent, per mille, etc.
private int multiplier = 1;
private String negativePrefix = "-";
private String negativeSuffix = "";
// Locale specific symbol collection.
private final NumberConstants numberConstants;
// The pattern to use for formatting and parsing.
private final String pattern;
private String positivePrefix = "";
private String positiveSuffix = "";
// True to force the use of exponential (i.e. scientific) notation.
private boolean useExponentialNotation = false;
/**
* Constructs a format object based on the specified settings.
*
* @param numberConstants the locale-specific number constants to use for this
* format -- **NOTE** subclasses passing their own instance here
* should pay attention to {@link #forcedLatinDigits()} and remap
* localized symbols using
* {@link #createLatinNumberConstants(NumberConstants)}
* @param pattern pattern that specify how number should be formatted
* @param cdata currency data that should be used
* @param userSuppliedPattern true if the pattern was supplied by the user
*/
protected NumberFormat(NumberConstants numberConstants, String pattern, CurrencyData cdata,
boolean userSuppliedPattern) {
if (cdata == null) {
throw new IllegalArgumentException("Unknown currency code");
}
this.numberConstants = numberConstants;
this.pattern = pattern;
currencyData = cdata;
// TODO: handle per-currency flags, such as symbol prefix/suffix and spacing
parsePattern(this.pattern);
if (!userSuppliedPattern && isCurrencyFormat) {
minimumFractionDigits = currencyData.getDefaultFractionDigits();
maximumFractionDigits = minimumFractionDigits;
}
}
/**
* Constructs a format object for the default locale based on the specified
* settings.
*
* @param pattern pattern that specify how number should be formatted
* @param cdata currency data that should be used
* @param userSuppliedPattern true if the pattern was supplied by the user
*/
protected NumberFormat(String pattern, CurrencyData cdata, boolean userSuppliedPattern) {
this(defaultNumberConstants, pattern, cdata, userSuppliedPattern);
}
/**
* This method formats a double to produce a string.
*
* @param number The double to format
* @return the formatted number string
*/
public String format(double number) {
if (Double.isNaN(number)) {
return numberConstants.notANumber();
}
boolean isNegative = ((number < 0.0)
|| (number == 0.0 && 1 / number < 0.0));
if (isNegative) {
number = -number;
}
StringBuilder buf = new StringBuilder();
if (Double.isInfinite(number)) {
buf.append(isNegative ? negativePrefix : positivePrefix);
buf.append(numberConstants.infinity());
buf.append(isNegative ? negativeSuffix : positiveSuffix);
return buf.toString();
}
number *= multiplier;
int scale = toScaledString(buf, number);
// pre-round value to deal with .15 being represented as .149999... etc
// check at 3 more digits than will be required in the output
int preRound = buf.length() + scale + maximumFractionDigits + 3;
if (preRound > 0 && preRound < buf.length()
&& buf.charAt(preRound) == '9') {
propagateCarry(buf, preRound - 1);
scale += buf.length() - preRound;
buf.delete(preRound, buf.length());
}
format(isNegative, buf, scale);
return buf.toString();
}
/**
* This method formats a Number to produce a string.
*
* Any {@link Number} which is not a {@link BigDecimal}, {@link BigInteger},
* or {@link Long} instance is formatted as a {@code double} value.
*
* @param number The Number instance to format
* @return the formatted number string
*/
public String format(Number number) {
if (number instanceof BigDecimal) {
BigDecimal bigDec = (BigDecimal) number;
boolean isNegative = bigDec.signum() < 0;
if (isNegative) {
bigDec = bigDec.negate();
}
bigDec = bigDec.multiply(BigDecimal.valueOf(multiplier));
StringBuilder buf = new StringBuilder();
buf.append(bigDec.unscaledValue().toString());
format(isNegative, buf, -bigDec.scale());
return buf.toString();
} else if (number instanceof BigInteger) {
BigInteger bigInt = (BigInteger) number;
boolean isNegative = bigInt.signum() < 0;
if (isNegative) {
bigInt = bigInt.negate();
}
bigInt = bigInt.multiply(BigInteger.valueOf(multiplier));
StringBuilder buf = new StringBuilder();
buf.append(bigInt.toString());
format(isNegative, buf, 0);
return buf.toString();
} else if (number instanceof Long) {
return format(number.longValue(), 0);
} else {
return format(number.doubleValue());
}
}
/**
* Returns the pattern used by this number format.
*/
public String getPattern() {
return pattern;
}
/**
* Change the number of fractional digits used for formatting with this
* instance.
*
* @param digits the exact number of fractional digits for formatted
* values; must be >= 0
* @return {@code this}, for chaining purposes
*/
public NumberFormat overrideFractionDigits(int digits) {
return overrideFractionDigits(digits, digits);
}
/**
* Change the number of fractional digits used for formatting with this
* instance. Digits after {@code minDigits} that are zero will be omitted from
* the formatted value.
*
* @param minDigits the minimum number of fractional digits for formatted
* values; must be >= 0
* @param maxDigits the maximum number of fractional digits for formatted
* values; must be >= {@code minDigits}
* @return {@code this}, for chaining purposes
*/
public NumberFormat overrideFractionDigits(int minDigits, int maxDigits) {
assert minDigits >= 0;
assert maxDigits >= minDigits;
minimumFractionDigits = minDigits;
maximumFractionDigits = maxDigits;
return this;
}
/**
* Parses text to produce a numeric value. A {@link NumberFormatException} is
* thrown if either the text is empty or if the parse does not consume all
* characters of the text.
*
* @param text the string being parsed
* @return a double value representing the parsed number
* @throws NumberFormatException if the entire text could not be converted
* into a double
*/
public double parse(String text) throws NumberFormatException {
int[] pos = {0};
double result = parse(text, pos);
if (pos[0] == 0 || pos[0] != text.length()) {
throw new NumberFormatException(text);
}
return result;
}
/**
* Parses text to produce a numeric value.
*
*
* The method attempts to parse text starting at the index given by pos. If
* parsing succeeds, then the index of pos
is updated to the
* index after the last character used (parsing does not necessarily use all
* characters up to the end of the string), and the parsed number is returned.
* The updated pos
can be used to indicate the starting point
* for the next call to this method. If an error occurs, then the index of
* pos
is not changed.
*
*
* @param text the string to be parsed
* @param inOutPos position to pass in and get back
* @return a double value representing the parsed number
* @throws NumberFormatException if the text segment could not be converted
* into a double
*/
public double parse(String text, int[] inOutPos) throws NumberFormatException {
double ret = 0.0;
boolean gotPositivePrefix = text.startsWith(positivePrefix, inOutPos[0]);
boolean gotNegativePrefix = text.startsWith(negativePrefix, inOutPos[0]);
boolean gotPositiveSuffix = text.endsWith(positiveSuffix);
boolean gotNegativeSuffix = text.endsWith(negativeSuffix);
boolean gotPositive = gotPositivePrefix && gotPositiveSuffix;
boolean gotNegative = gotNegativePrefix && gotNegativeSuffix;
// Handle conflicts where we get both patterns, which usually
// happens when one is a prefix of the other (such as the positive
// pattern having empty prefix/suffixes).
if (gotPositive && gotNegative) {
if (positivePrefix.length() > negativePrefix.length()) {
gotNegative = false;
} else if (positivePrefix.length() < negativePrefix.length()) {
gotPositive = false;
} else if (positiveSuffix.length() > negativeSuffix.length()) {
gotNegative = false;
} else if (positiveSuffix.length() < negativeSuffix.length()) {
gotPositive = false;
} else {
// can't tell patterns apart, must be positive
gotNegative = false;
}
} else if (!gotPositive && !gotNegative) {
throw new NumberFormatException(text
+ " does not have either positive or negative affixes");
}
// Contains just the value to parse, stripping any prefix or suffix
String valueOnly = null;
if (gotPositive) {
inOutPos[0] += positivePrefix.length();
valueOnly = text.substring(inOutPos[0],
text.length() - positiveSuffix.length());
} else {
inOutPos[0] += negativePrefix.length();
valueOnly = text.substring(inOutPos[0],
text.length() - negativeSuffix.length());
}
// Process digits or special values, and find decimal position.
if (valueOnly.equals(numberConstants.infinity())) {
inOutPos[0] += numberConstants.infinity().length();
ret = Double.POSITIVE_INFINITY;
} else if (valueOnly.equals(numberConstants.notANumber())) {
inOutPos[0] += numberConstants.notANumber().length();
ret = Double.NaN;
} else {
int[] tempPos = {0};
ret = parseNumber(valueOnly, tempPos);
inOutPos[0] += tempPos[0];
}
// Check for suffix.
if (gotPositive) {
inOutPos[0] += positiveSuffix.length();
} else if (gotNegative) {
inOutPos[0] += negativeSuffix.length();
}
if (gotNegative) {
ret = -ret;
}
return ret;
}
/**
* Format a number with its significant digits already represented in string
* form. This is done so both double and BigInteger/Decimal formatting can
* share code without requiring all users to pay the code size penalty for
* BigDecimal/etc.
*
* Example values passed in:
*
* - -13e2
*
{@code isNegative=true, digits="13", scale=2}
* - 3.14158
*
{@code isNegative=false, digits="314158", scale=-5}
* - .0001
*
{@code isNegative=false, digits="1" ("0001" would be ok), scale=-4}
*
*
* @param isNegative true if the value to be formatted is negative
* @param digits a StringBuilder containing just the significant digits in
* the value to be formatted, the formatted result will be left here
* @param scale the number of places to the right the decimal point should
* be moved in the digit string -- negative means the value contains
* fractional digits
*/
protected void format(boolean isNegative, StringBuilder digits, int scale) {
char decimalSeparator;
char groupingSeparator;
if (isCurrencyFormat) {
decimalSeparator = numberConstants.monetarySeparator().charAt(0);
groupingSeparator = numberConstants.monetaryGroupingSeparator().charAt(0);
} else {
decimalSeparator = numberConstants.decimalSeparator().charAt(0);
groupingSeparator = numberConstants.groupingSeparator().charAt(0);
}
// Set these transient fields, which will be adjusted/used by the routines
// called in this method.
exponent = 0;
digitsLength = digits.length();
decimalPosition = digitsLength + scale;
boolean useExponent = this.useExponentialNotation;
int currentGroupingSize = this.groupingSize;
if (decimalPosition > 1024) {
// force really large numbers to be in exponential form
useExponent = true;
}
if (useExponent) {
computeExponent(digits);
}
processLeadingZeros(digits);
roundValue(digits);
insertGroupingSeparators(digits, groupingSeparator, currentGroupingSize);
adjustFractionDigits(digits);
addZeroAndDecimal(digits, decimalSeparator);
if (useExponent) {
addExponent(digits);
// the above call has invalidated digitsLength == digits.length()
}
char zeroChar = numberConstants.zeroDigit().charAt(0);
if (zeroChar != '0') {
localizeDigits(digits, zeroChar);
}
// add prefix/suffix
digits.insert(0, isNegative ? negativePrefix : positivePrefix);
digits.append(isNegative ? negativeSuffix : positiveSuffix);
}
/**
* Parses text to produce a numeric value. A {@link NumberFormatException} is
* thrown if either the text is empty or if the parse does not consume all
* characters of the text.
*
* param text the string to be parsed
* return a parsed number value, which may be a Double, BigInteger, or
* BigDecimal
* throws NumberFormatException if the text segment could not be converted
* into a number
*/
// public Number parseBig(String text) throws NumberFormatException {
// // TODO(jat): implement
// return Double.valueOf(parse(text));
// }
/**
* Parses text to produce a numeric value.
*
*
* The method attempts to parse text starting at the index given by pos. If
* parsing succeeds, then the index of pos
is updated to the
* index after the last character used (parsing does not necessarily use all
* characters up to the end of the string), and the parsed number is returned.
* The updated pos
can be used to indicate the starting point
* for the next call to this method. If an error occurs, then the index of
* pos
is not changed.
*
*
* param text the string to be parsed
* pparam inOutPos position to pass in and get back
* return a parsed number value, which may be a Double, BigInteger, or
* BigDecimal
* throws NumberFormatException if the text segment could not be converted
* into a number
*/
// public Number parseBig(String text, int[] inOutPos)
// throws NumberFormatException {
// // TODO(jat): implement
// return Double.valueOf(parse(text, inOutPos));
// }
/**
* Format a possibly scaled long value.
*
* @param value value to format
* @param scale the number of places to the right the decimal point should
* be moved in the digit string -- negative means the value contains
* fractional digits
* @return formatted value
*/
protected String format(long value, int scale) {
boolean isNegative = value < 0;
if (isNegative) {
value = -value;
}
value *= multiplier;
StringBuilder buf = new StringBuilder();
buf.append(String.valueOf(value));
format(isNegative, buf, scale);
return buf.toString();
}
/**
* Returns the number of digits between grouping separators in the integer
* portion of a number.
*/
protected int getGroupingSize() {
return groupingSize;
}
/**
* Returns the prefix to use for negative values.
*/
protected String getNegativePrefix() {
return negativePrefix;
}
/**
* Returns the suffix to use for negative values.
*/
protected String getNegativeSuffix() {
return negativeSuffix;
}
/**
* Returns the NumberConstants instance for this formatter.
*/
protected NumberConstants getNumberConstants() {
return numberConstants;
}
/**
* Returns the prefix to use for positive values.
*/
protected String getPositivePrefix() {
return positivePrefix;
}
/**
* Returns the suffix to use for positive values.
*/
protected String getPositiveSuffix() {
return positiveSuffix;
}
/**
* Returns true if the decimal separator should always be shown.
*/
protected boolean isDecimalSeparatorAlwaysShown() {
return decimalSeparatorAlwaysShown;
}
/**
* Add exponent suffix.
*
* @param digits
*/
private void addExponent(StringBuilder digits) {
digits.append(numberConstants.exponentialSymbol());
if (exponent < 0) {
exponent = -exponent;
digits.append(numberConstants.minusSign());
}
String exponentDigits = String.valueOf(exponent);
for (int i = exponentDigits.length(); i < minExponentDigits; ++i) {
digits.append('0');
}
digits.append(exponentDigits);
}
/**
* @param digits
* @param decimalSeparator
*/
private void addZeroAndDecimal(StringBuilder digits, char decimalSeparator) {
// add zero and decimal point if required
if (digitsLength == 0) {
digits.insert(0, '0');
++decimalPosition;
++digitsLength;
}
if (decimalPosition < digitsLength || decimalSeparatorAlwaysShown) {
digits.insert(decimalPosition, decimalSeparator);
++digitsLength;
}
}
/**
* Adjust the fraction digits, adding trailing zeroes if necessary or removing
* excess trailing zeroes.
*
* @param digits
*/
private void adjustFractionDigits(StringBuilder digits) {
// adjust fraction digits as required
int requiredDigits = decimalPosition + minimumFractionDigits;
if (digitsLength < requiredDigits) {
// add trailing zeros
while (digitsLength < requiredDigits) {
digits.append('0');
++digitsLength;
}
} else {
// remove excess trailing zeros
int toRemove = decimalPosition + maximumFractionDigits;
if (toRemove > digitsLength) {
toRemove = digitsLength;
}
while (toRemove > requiredDigits
&& digits.charAt(toRemove - 1) == '0') {
--toRemove;
}
if (toRemove < digitsLength) {
digits.delete(toRemove, digitsLength);
digitsLength = toRemove;
}
}
}
/**
* Compute the exponent to use and adjust decimal position if we are using
* exponential notation.
*
* @param digits
*/
private void computeExponent(StringBuilder digits) {
// always trim leading zeros
int strip = 0;
while (strip < digitsLength - 1 && digits.charAt(strip) == '0') {
++strip;
}
if (strip > 0) {
digits.delete(0, strip);
digitsLength -= strip;
exponent -= strip;
}
// decimal should wind up between minimum & maximumIntegerDigits
if (maximumIntegerDigits > minimumIntegerDigits
&& maximumIntegerDigits > 0) {
// in this case, the exponent should be a multiple of
// maximumIntegerDigits and 1 <= decimal <= maximumIntegerDigits
exponent += decimalPosition - 1;
int remainder = exponent % maximumIntegerDigits;
if (remainder < 0) {
remainder += maximumIntegerDigits;
}
decimalPosition = remainder + 1;
exponent -= remainder;
} else {
exponent += decimalPosition - minimumIntegerDigits;
decimalPosition = minimumIntegerDigits;
}
// special-case 0 to have an exponent of 0
if (digitsLength == 1 && digits.charAt(0) == '0') {
exponent = 0;
decimalPosition = minimumIntegerDigits;
}
}
/**
* This method return the digit that represented by current character, it
* could be either '0' to '9', or a locale specific digit.
*
* @param ch character that represents a digit
* @return the digit value
*/
private int getDigit(char ch) {
if ('0' <= ch && ch <= '0' + 9) {
return (ch - '0');
} else {
char zeroChar = numberConstants.zeroDigit().charAt(0);
return ((zeroChar <= ch && ch <= zeroChar + 9) ? (ch - zeroChar) : -1);
}
}
/**
* Insert grouping separators if needed.
*
* @param digits
* @param groupingSeparator
* @param g
*/
private void insertGroupingSeparators(StringBuilder digits,
char groupingSeparator, int g) {
if (g > 0) {
for (int i = g; i < decimalPosition; i += g + 1) {
digits.insert(decimalPosition - i, groupingSeparator);
++decimalPosition;
++digitsLength;
}
}
}
/**
* Replace locale-independent digits with locale-specific ones.
*
* @param digits StringBuilder containing formatted number
* @param zero locale-specific zero character -- the rest of the digits must
* be consecutive
*/
private void localizeDigits(StringBuilder digits, char zero) {
// don't use digitsLength since we may have added an exponent
int n = digits.length();
for (int i = 0; i < n; ++i) {
char ch = digits.charAt(i);
if (ch >= '0' && ch <= '9') {
digits.setCharAt(i, (char) (ch - '0' + zero));
}
}
}
/**
* This method parses affix part of pattern.
*
* @param pattern pattern string that need to be parsed
* @param start start position to parse
* @param affix store the parsed result
* @param inNegativePattern true if we are parsing the negative pattern and
* therefore only care about the prefix and suffix
* @return how many characters parsed
*/
private int parseAffix(String pattern, int start, StringBuilder affix,
boolean inNegativePattern) {
affix.delete(0, affix.length());
boolean inQuote = false;
int len = pattern.length();
for (int pos = start; pos < len; ++pos) {
char ch = pattern.charAt(pos);
if (ch == QUOTE) {
if ((pos + 1) < len && pattern.charAt(pos + 1) == QUOTE) {
++pos;
affix.append("'"); // 'don''t'
} else {
inQuote = !inQuote;
}
continue;
}
if (inQuote) {
affix.append(ch);
} else {
switch (ch) {
case PATTERN_DIGIT:
case PATTERN_ZERO_DIGIT:
case PATTERN_GROUPING_SEPARATOR:
case PATTERN_DECIMAL_SEPARATOR:
case PATTERN_SEPARATOR:
return pos - start;
case CURRENCY_SIGN:
isCurrencyFormat = true;
if ((pos + 1) < len && pattern.charAt(pos + 1) == CURRENCY_SIGN) {
++pos;
if (pos < len - 2 && pattern.charAt(pos + 1) == CURRENCY_SIGN
&& pattern.charAt(pos + 2) == CURRENCY_SIGN) {
pos += 2;
affix.append(currencyData.getSimpleCurrencySymbol());
} else {
affix.append(currencyData.getCurrencyCode());
}
} else {
affix.append(currencyData.getCurrencySymbol());
}
break;
case PATTERN_PERCENT:
if (!inNegativePattern) {
if (multiplier != 1) {
throw new IllegalArgumentException(
"Too many percent/per mille characters in pattern \""
+ pattern + '"');
}
multiplier = 100;
}
affix.append(numberConstants.percent());
break;
case PATTERN_PER_MILLE:
if (!inNegativePattern) {
if (multiplier != 1) {
throw new IllegalArgumentException(
"Too many percent/per mille characters in pattern \""
+ pattern + '"');
}
multiplier = 1000;
}
affix.append(numberConstants.perMill());
break;
case PATTERN_MINUS:
affix.append("-");
break;
default:
affix.append(ch);
}
}
}
return len - start;
}
/**
* This function parses a "localized" text into a double
. It
* needs to handle locale specific decimal, grouping, exponent and digit.
*
* @param text the text that need to be parsed
* @param pos in/out parsing position. in case of failure, this shouldn't be
* changed
* @return double value, could be 0.0 if nothing can be parsed
*/
private double parseNumber(String text, int[] pos) {
double ret;
boolean sawDecimal = false;
boolean sawExponent = false;
boolean sawDigit = false;
int scale = 1;
String decimal = isCurrencyFormat ? numberConstants.monetarySeparator()
: numberConstants.decimalSeparator();
String grouping = isCurrencyFormat
? numberConstants.monetaryGroupingSeparator()
: numberConstants.groupingSeparator();
String exponentChar = numberConstants.exponentialSymbol();
StringBuilder normalizedText = new StringBuilder();
for (; pos[0] < text.length(); ++pos[0]) {
char ch = text.charAt(pos[0]);
int digit = getDigit(ch);
if (digit >= 0 && digit <= 9) {
normalizedText.append((char) (digit + '0'));
sawDigit = true;
} else if (ch == decimal.charAt(0)) {
if (sawDecimal || sawExponent) {
break;
}
normalizedText.append('.');
sawDecimal = true;
} else if (ch == grouping.charAt(0)) {
if (sawDecimal || sawExponent) {
break;
}
continue;
} else if (ch == exponentChar.charAt(0)) {
if (sawExponent) {
break;
}
normalizedText.append('E');
sawExponent = true;
} else if (ch == '+' || ch == '-') {
normalizedText.append(ch);
} else if (ch == numberConstants.percent().charAt(0)) {
if (scale != 1) {
break;
}
scale = 100;
if (sawDigit) {
++pos[0];
break;
}
} else if (ch == numberConstants.perMill().charAt(0)) {
if (scale != 1) {
break;
}
scale = 1000;
if (sawDigit) {
++pos[0];
break;
}
} else {
break;
}
}
// parseDouble could throw NumberFormatException, rethrow with correct text.
try {
ret = Double.parseDouble(normalizedText.toString());
} catch (NumberFormatException e) {
throw new NumberFormatException(text);
}
ret = ret / scale;
return ret;
}
/**
* Method parses provided pattern, result is stored in member variables.
*
* @param pattern
*/
private void parsePattern(String pattern) {
int pos = 0;
StringBuilder affix = new StringBuilder();
pos += parseAffix(pattern, pos, affix, false);
positivePrefix = affix.toString();
pos += parseTrunk(pattern, pos, false);
pos += parseAffix(pattern, pos, affix, false);
positiveSuffix = affix.toString();
if (pos < pattern.length() && pattern.charAt(pos) == PATTERN_SEPARATOR) {
++pos;
pos += parseAffix(pattern, pos, affix, true);
negativePrefix = affix.toString();
// the negative pattern is only used for prefix/suffix
pos += parseTrunk(pattern, pos, true);
pos += parseAffix(pattern, pos, affix, true);
negativeSuffix = affix.toString();
} else {
negativePrefix = numberConstants.minusSign() + positivePrefix;
negativeSuffix = positiveSuffix;
}
}
/**
* This method parses the trunk part of a pattern.
*
* @param pattern pattern string that need to be parsed
* @param start where parse started
* @param ignorePattern true if we are only parsing this for length
* and correctness, such as in the negative portion of the pattern
* @return how many characters parsed
*/
private int parseTrunk(String pattern, int start, boolean ignorePattern) {
int decimalPos = -1;
int digitLeftCount = 0, zeroDigitCount = 0, digitRightCount = 0;
byte groupingCount = -1;
int len = pattern.length();
int pos = start;
boolean loop = true;
for (; (pos < len) && loop; ++pos) {
char ch = pattern.charAt(pos);
switch (ch) {
case PATTERN_DIGIT:
if (zeroDigitCount > 0) {
++digitRightCount;
} else {
++digitLeftCount;
}
if (groupingCount >= 0 && decimalPos < 0) {
++groupingCount;
}
break;
case PATTERN_ZERO_DIGIT:
if (digitRightCount > 0) {
throw new IllegalArgumentException("Unexpected '0' in pattern \""
+ pattern + '"');
}
++zeroDigitCount;
if (groupingCount >= 0 && decimalPos < 0) {
++groupingCount;
}
break;
case PATTERN_GROUPING_SEPARATOR:
groupingCount = 0;
break;
case PATTERN_DECIMAL_SEPARATOR:
if (decimalPos >= 0) {
throw new IllegalArgumentException(
"Multiple decimal separators in pattern \"" + pattern + '"');
}
decimalPos = digitLeftCount + zeroDigitCount + digitRightCount;
break;
case PATTERN_EXPONENT:
if (!ignorePattern) {
if (useExponentialNotation) {
throw new IllegalArgumentException("Multiple exponential "
+ "symbols in pattern \"" + pattern + '"');
}
useExponentialNotation = true;
minExponentDigits = 0;
}
// Use lookahead to parse out the exponential part
// of the pattern, then jump into phase 2.
while ((pos + 1) < len
&& pattern.charAt(pos + 1) == PATTERN_ZERO_DIGIT) {
++pos;
if (!ignorePattern) {
++minExponentDigits;
}
}
if (!ignorePattern && (digitLeftCount + zeroDigitCount) < 1
|| minExponentDigits < 1) {
throw new IllegalArgumentException("Malformed exponential "
+ "pattern \"" + pattern + '"');
}
loop = false;
break;
default:
--pos;
loop = false;
break;
}
}
if (zeroDigitCount == 0 && digitLeftCount > 0 && decimalPos >= 0) {
// Handle "###.###" and "###." and ".###".
int n = decimalPos;
if (n == 0) { // Handle ".###"
++n;
}
digitRightCount = digitLeftCount - n;
digitLeftCount = n - 1;
zeroDigitCount = 1;
}
// Do syntax checking on the digits.
if ((decimalPos < 0 && digitRightCount > 0)
|| (decimalPos >= 0 && (decimalPos < digitLeftCount || decimalPos > (digitLeftCount + zeroDigitCount)))
|| groupingCount == 0) {
throw new IllegalArgumentException("Malformed pattern \"" + pattern + '"');
}
if (ignorePattern) {
return pos - start;
}
int totalDigits = digitLeftCount + zeroDigitCount + digitRightCount;
maximumFractionDigits = (decimalPos >= 0 ? (totalDigits - decimalPos) : 0);
if (decimalPos >= 0) {
minimumFractionDigits = digitLeftCount + zeroDigitCount - decimalPos;
if (minimumFractionDigits < 0) {
minimumFractionDigits = 0;
}
}
/*
* The effectiveDecimalPos is the position the decimal is at or would be at
* if there is no decimal. Note that if decimalPos<0, then digitTotalCount ==
* digitLeftCount + zeroDigitCount.
*/
int effectiveDecimalPos = decimalPos >= 0 ? decimalPos : totalDigits;
minimumIntegerDigits = effectiveDecimalPos - digitLeftCount;
if (useExponentialNotation) {
maximumIntegerDigits = digitLeftCount + minimumIntegerDigits;
// In exponential display, integer part can't be empty.
if (maximumFractionDigits == 0 && minimumIntegerDigits == 0) {
minimumIntegerDigits = 1;
}
}
this.groupingSize = (groupingCount > 0) ? groupingCount : 0;
decimalSeparatorAlwaysShown = (decimalPos == 0 || decimalPos == totalDigits);
return pos - start;
}
/**
* Remove excess leading zeros or add some if we don't have enough.
*
* @param digits
*/
private void processLeadingZeros(StringBuilder digits) {
// make sure we have enough trailing zeros
if (decimalPosition > digitsLength) {
while (digitsLength < decimalPosition) {
digits.append('0');
++digitsLength;
}
}
if (!useExponentialNotation) {
// make sure we have the right number of leading zeros
if (decimalPosition < minimumIntegerDigits) {
// add leading zeros
StringBuilder prefix = new StringBuilder();
while (decimalPosition < minimumIntegerDigits) {
prefix.append('0');
++decimalPosition;
++digitsLength;
}
digits.insert(0, prefix);
} else if (decimalPosition > minimumIntegerDigits) {
// trim excess leading zeros
int strip = decimalPosition - minimumIntegerDigits;
for (int i = 0; i < strip; ++i) {
if (digits.charAt(i) != '0') {
strip = i;
break;
}
}
if (strip > 0) {
digits.delete(0, strip);
digitsLength -= strip;
decimalPosition -= strip;
}
}
}
}
/**
* Propagate a carry from incrementing the {@code i+1}'th digit.
*
* @param digits
* @param i digit to start incrementing
*/
private void propagateCarry(StringBuilder digits, int i) {
boolean carry = true;
while (carry && i >= 0) {
char digit = digits.charAt(i);
if (digit == '9') {
// set this to zero and keep going
digits.setCharAt(i--, '0');
} else {
digits.setCharAt(i, (char) (digit + 1));
carry = false;
}
}
if (carry) {
// ran off the front, prepend a 1
digits.insert(0, '1');
++decimalPosition;
++digitsLength;
}
}
/**
* Round the value at the requested place, propagating any carry backward.
*
* @param digits
*/
private void roundValue(StringBuilder digits) {
// TODO(jat): other rounding modes?
if (digitsLength > decimalPosition + maximumFractionDigits
&& digits.charAt(decimalPosition + maximumFractionDigits) >= '5') {
int i = decimalPosition + maximumFractionDigits - 1;
propagateCarry(digits, i);
}
}
}