All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.gwt.i18n.client.NumberFormat Maven / Gradle / Ivy

There is a newer version: 2.10.0
Show newest version
/*
 * 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. *

* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
SymbolLocationLocalized?Meaning
0NumberYesDigit
#NumberYesDigit, zero shows as absent
.NumberYesDecimal separator or monetary decimal separator
-NumberYesMinus sign
,NumberYesGrouping separator
ENumberYesSeparates mantissa and exponent in scientific notation; need not be * quoted in prefix or suffix
;Subpattern boundaryYesSeparates positive and negative subpatterns
%Prefix or suffixYesMultiply by 100 and show as percentage
\u2030 (\u005Cu2030)Prefix or suffixYesMultiply by 1000 and show as per mille
\u00A4 (\u005Cu00A4)Prefix or suffixNoCurrency 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 suffixNoUsed 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|Yeither X or Y
C..Dany character from C up to D, inclusive
S-Tcharacters 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); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy