// DecimalFormat.java - Localized number formatting. /* Copyright (C) 1999 Cygnus Solutions This file is part of libgcj. This software is copyrighted work licensed under the terms of the Libgcj License. Please consult the file "LIBGCJ_LICENSE" for details. */ package java.text; import java.util.Locale; import java.util.MissingResourceException; import java.util.ResourceBundle; /** * @author Tom Tromey * @date March 4, 1999 */ /* Written using "Java Class Libraries", 2nd edition, plus online * API docs for JDK 1.2 from http://www.javasoft.com. * Status: Believed complete and correct to 1.2, except serialization. * Note however that the docs are very unclear about how format parsing * should work. No doubt there are problems here. */ public class DecimalFormat extends NumberFormat { // This is a helper for applyPatternWithSymbols. It reads a prefix // or a suffix. It can cause some side-effects. private final int scanFix (String pattern, int index, StringBuffer buf, String patChars, DecimalFormatSymbols syms, boolean is_suffix) { int len = pattern.length(); buf.setLength(0); boolean multiplierSet = false; while (index < len) { char c = pattern.charAt(index); if (c == '\'' && index + 1 < len && pattern.charAt(index + 1) == '\'') { buf.append(c); ++index; } else if (c == '\'' && index + 2 < len && pattern.charAt(index + 2) == '\'') { buf.append(pattern.charAt(index + 1)); index += 2; } else if (c == '\u00a4') { if (index + 1 < len && pattern.charAt(index + 1) == '\u00a4') { buf.append(syms.getInternationalCurrencySymbol()); ++index; } else buf.append(syms.getCurrencySymbol()); } else if (is_suffix && c == syms.getPercent()) { if (multiplierSet) throw new IllegalArgumentException ("multiplier already set " + "- index: " + index); multiplierSet = true; multiplier = 100; buf.append(c); } else if (is_suffix && c == syms.getPerMill()) { if (multiplierSet) throw new IllegalArgumentException ("multiplier already set " + "- index: " + index); multiplierSet = true; multiplier = 1000; buf.append(c); } else if (patChars.indexOf(c) != -1) { // This is a pattern character. break; } else buf.append(c); ++index; } return index; } // A helper which reads a number format. private final int scanFormat (String pattern, int index, String patChars, DecimalFormatSymbols syms, boolean is_positive) { int max = pattern.length(); int countSinceGroup = 0; int zeroCount = 0; boolean saw_group = false; // // Scan integer part. // while (index < max) { char c = pattern.charAt(index); if (c == syms.getDigit()) { if (zeroCount > 0) throw new IllegalArgumentException ("digit mark following " + "zero - index: " + index); ++countSinceGroup; } else if (c == syms.getZeroDigit()) { ++zeroCount; ++countSinceGroup; } else if (c == syms.getGroupingSeparator()) { countSinceGroup = 0; saw_group = true; } else break; ++index; } // We can only side-effect when parsing the positive format. if (is_positive) { groupingUsed = saw_group; groupingSize = (byte) countSinceGroup; minimumIntegerDigits = zeroCount; } // Early termination. if (index == max || pattern.charAt(index) == syms.getGroupingSeparator()) { if (is_positive) decimalSeparatorAlwaysShown = false; return index; } if (pattern.charAt(index) == syms.getDecimalSeparator()) { ++index; // // Scan fractional part. // int hashCount = 0; zeroCount = 0; while (index < max) { char c = pattern.charAt(index); if (c == syms.getZeroDigit()) { if (hashCount > 0) throw new IllegalArgumentException ("zero mark " + "following digit - index: " + index); ++zeroCount; } else if (c == syms.getDigit()) { ++hashCount; } else if (c != syms.getExponential() && c != syms.getPatternSeparator() && patChars.indexOf(c) != -1) throw new IllegalArgumentException ("unexpected special " + "character - index: " + index); else break; ++index; } if (is_positive) { maximumFractionDigits = hashCount + zeroCount; minimumFractionDigits = zeroCount; } if (index == max) return index; } if (pattern.charAt(index) == syms.getExponential()) { // // Scan exponential format. // zeroCount = 0; ++index; while (index < max) { char c = pattern.charAt(index); if (c == syms.getZeroDigit()) ++zeroCount; else if (c == syms.getDigit()) { if (zeroCount > 0) throw new IllegalArgumentException ("digit mark following zero " + "in exponent - index: " + index); } else if (patChars.indexOf(c) != -1) throw new IllegalArgumentException ("unexpected special " + "character - index: " + index); else break; ++index; } if (is_positive) { useExponentialNotation = true; minExponentDigits = (byte) zeroCount; } } return index; } // This helper function creates a string consisting of all the // characters which can appear in a pattern and must be quoted. private final String patternChars (DecimalFormatSymbols syms) { StringBuffer buf = new StringBuffer (); buf.append(syms.getDecimalSeparator()); buf.append(syms.getDigit()); buf.append(syms.getExponential()); buf.append(syms.getGroupingSeparator()); // Adding this one causes pattern application to fail. // Of course, omitting is causes toPattern to fail. // ... but we already have bugs there. FIXME. // buf.append(syms.getMinusSign()); buf.append(syms.getPatternSeparator()); buf.append(syms.getPercent()); buf.append(syms.getPerMill()); buf.append(syms.getZeroDigit()); buf.append('\u00a4'); return buf.toString(); } private final void applyPatternWithSymbols (String pattern, DecimalFormatSymbols syms) { // Initialize to the state the parser expects. negativePrefix = ""; negativeSuffix = ""; positivePrefix = ""; positiveSuffix = ""; decimalSeparatorAlwaysShown = false; groupingSize = 0; minExponentDigits = 0; multiplier = 1; useExponentialNotation = false; groupingUsed = false; maximumFractionDigits = 0; maximumIntegerDigits = 309; minimumFractionDigits = 0; minimumIntegerDigits = 1; StringBuffer buf = new StringBuffer (); String patChars = patternChars (syms); int max = pattern.length(); int index = scanFix (pattern, 0, buf, patChars, syms, false); positivePrefix = buf.toString(); index = scanFormat (pattern, index, patChars, syms, true); index = scanFix (pattern, index, buf, patChars, syms, true); positiveSuffix = buf.toString(); if (index == pattern.length()) { // No negative info. negativePrefix = null; negativeSuffix = null; } else { if (pattern.charAt(index) != syms.getPatternSeparator()) throw new IllegalArgumentException ("separator character " + "expected - index: " + index); index = scanFix (pattern, index + 1, buf, patChars, syms, false); negativePrefix = buf.toString(); // We parse the negative format for errors but we don't let // it side-effect this object. index = scanFormat (pattern, index, patChars, syms, false); index = scanFix (pattern, index, buf, patChars, syms, true); negativeSuffix = buf.toString(); if (index != pattern.length()) throw new IllegalArgumentException ("end of pattern expected " + "- index: " + index); } } public void applyLocalizedPattern (String pattern) { // JCL p. 638 claims this throws a ParseException but p. 629 // contradicts this. Empirical tests with patterns of "0,###.0" // and "#.#.#" corroborate the p. 629 statement that an // IllegalArgumentException is thrown. applyPatternWithSymbols (pattern, symbols); } public void applyPattern (String pattern) { // JCL p. 638 claims this throws a ParseException but p. 629 // contradicts this. Empirical tests with patterns of "0,###.0" // and "#.#.#" corroborate the p. 629 statement that an // IllegalArgumentException is thrown. applyPatternWithSymbols (pattern, nonLocalizedSymbols); } public Object clone () { return new DecimalFormat (this); } private DecimalFormat (DecimalFormat dup) { decimalSeparatorAlwaysShown = dup.decimalSeparatorAlwaysShown; groupingSize = dup.groupingSize; minExponentDigits = dup.minExponentDigits; multiplier = dup.multiplier; negativePrefix = dup.negativePrefix; negativeSuffix = dup.negativeSuffix; positivePrefix = dup.positivePrefix; positiveSuffix = dup.positiveSuffix; symbols = (DecimalFormatSymbols) dup.symbols.clone(); useExponentialNotation = dup.useExponentialNotation; } public DecimalFormat () { this ("#,##0.###"); } public DecimalFormat (String pattern) { this (pattern, new DecimalFormatSymbols ()); } public DecimalFormat (String pattern, DecimalFormatSymbols symbols) { this.symbols = symbols; applyPattern (pattern); } private final boolean equals (String s1, String s2) { if (s1 == null || s2 == null) return s1 == s2; return s1.equals(s2); } public boolean equals (Object obj) { if (! (obj instanceof DecimalFormat)) return false; DecimalFormat dup = (DecimalFormat) obj; return (decimalSeparatorAlwaysShown == dup.decimalSeparatorAlwaysShown && groupingSize == dup.groupingSize && minExponentDigits == dup.minExponentDigits && multiplier == dup.multiplier && equals(negativePrefix, dup.negativePrefix) && equals(negativeSuffix, dup.negativeSuffix) && equals(positivePrefix, dup.positivePrefix) && equals(positiveSuffix, dup.positiveSuffix) && symbols.equals(dup.symbols) && useExponentialNotation == dup.useExponentialNotation); } public StringBuffer format (double number, StringBuffer dest, FieldPosition fieldPos) { // A very special case. if (Double.isNaN(number)) { dest.append(symbols.getNaN()); if (fieldPos != null && fieldPos.getField() == INTEGER_FIELD) { int index = dest.length(); fieldPos.setBeginIndex(index - symbols.getNaN().length()); fieldPos.setEndIndex(index); } return dest; } boolean is_neg = number < 0; if (is_neg) { if (negativePrefix != null) dest.append(negativePrefix); else { dest.append(symbols.getMinusSign()); dest.append(positivePrefix); } number = - number; } else dest.append(positivePrefix); int integerBeginIndex = dest.length(); int integerEndIndex = 0; if (Double.isInfinite (number)) { dest.append(symbols.getInfinity()); integerEndIndex = dest.length(); } else { number *= multiplier; // Compute exponent. long exponent = 0; double baseNumber; if (useExponentialNotation) { exponent = (long) (Math.log(number) / Math.log(10)); if (minimumIntegerDigits > 0) exponent -= minimumIntegerDigits - 1; baseNumber = (long) (number / Math.pow(10.0, exponent)); } else baseNumber = number; // Round to the correct number of digits. baseNumber += 5 * Math.pow(10.0, - maximumFractionDigits - 1); int index = dest.length(); double intPart = Math.floor(baseNumber); int count = 0; while (count < maximumIntegerDigits && (intPart > 0 || count < minimumIntegerDigits)) { long dig = (long) (intPart % 10); intPart = Math.floor(intPart / 10); // Append group separator if required. if (groupingUsed && count > 0 && count % groupingSize == 0) dest.insert(index, symbols.getGroupingSeparator()); dest.insert(index, (char) (symbols.getZeroDigit() + dig)); ++count; } integerEndIndex = dest.length(); int decimal_index = integerEndIndex; int consecutive_zeros = 0; int total_digits = 0; // Strip integer part from NUMBER. double fracPart = baseNumber - Math.floor(baseNumber); for (count = 0; count < maximumFractionDigits && (fracPart != 0 || count < minimumFractionDigits); ++count) { ++total_digits; fracPart *= 10; long dig = (long) fracPart; if (dig == 0) ++consecutive_zeros; else consecutive_zeros = 0; dest.append((char) (symbols.getZeroDigit() + dig)); // Strip integer part from FRACPART. fracPart = fracPart - Math.floor (fracPart); } // Strip extraneous trailing `0's. We can't always detect // these in the loop. int extra_zeros = Math.min (consecutive_zeros, total_digits - minimumFractionDigits); if (extra_zeros > 0) { dest.setLength(dest.length() - extra_zeros); total_digits -= extra_zeros; } // If required, add the decimal symbol. if (decimalSeparatorAlwaysShown || total_digits > 0) { dest.insert(decimal_index, symbols.getDecimalSeparator()); if (fieldPos != null && fieldPos.getField() == FRACTION_FIELD) { fieldPos.setBeginIndex(decimal_index + 1); fieldPos.setEndIndex(dest.length()); } } // Finally, print the exponent. if (useExponentialNotation) { dest.append(symbols.getExponential()); dest.append(exponent < 0 ? '-' : '+'); index = dest.length(); for (count = 0; exponent > 0 || count < minExponentDigits; ++count) { long dig = exponent % 10; exponent /= 10; dest.insert(index, (char) (symbols.getZeroDigit() + dig)); } } } if (fieldPos != null && fieldPos.getField() == INTEGER_FIELD) { fieldPos.setBeginIndex(integerBeginIndex); fieldPos.setEndIndex(integerEndIndex); } dest.append((is_neg && negativeSuffix != null) ? negativeSuffix : positiveSuffix); return dest; } public StringBuffer format (long number, StringBuffer dest, FieldPosition fieldPos) { // If using exponential notation, we just format as a double. if (useExponentialNotation) return format ((double) number, dest, fieldPos); boolean is_neg = number < 0; if (is_neg) { if (negativePrefix != null) dest.append(negativePrefix); else { dest.append(symbols.getMinusSign()); dest.append(positivePrefix); } number = - number; } else dest.append(positivePrefix); int integerBeginIndex = dest.length(); int index = dest.length(); int count = 0; while (count < maximumIntegerDigits && (number > 0 || count < minimumIntegerDigits)) { long dig = number % 10; number /= 10; // NUMBER and DIG will be less than 0 if the original number // was the most negative long. if (dig < 0) { dig = - dig; number = - number; } // Append group separator if required. if (groupingUsed && count > 0 && count % groupingSize == 0) dest.insert(index, symbols.getGroupingSeparator()); dest.insert(index, (char) (symbols.getZeroDigit() + dig)); ++count; } if (fieldPos != null && fieldPos.getField() == INTEGER_FIELD) { fieldPos.setBeginIndex(integerBeginIndex); fieldPos.setEndIndex(dest.length()); } if (decimalSeparatorAlwaysShown || minimumFractionDigits > 0) { dest.append(symbols.getDecimalSeparator()); if (fieldPos != null && fieldPos.getField() == FRACTION_FIELD) { fieldPos.setBeginIndex(dest.length()); fieldPos.setEndIndex(dest.length() + minimumFractionDigits); } } for (count = 0; count < minimumFractionDigits; ++count) dest.append(symbols.getZeroDigit()); dest.append((is_neg && negativeSuffix != null) ? negativeSuffix : positiveSuffix); return dest; } public DecimalFormatSymbols getDecimalFormatSymbols () { return symbols; } public int getGroupingSize () { return groupingSize; } public int getMultiplier () { return multiplier; } public String getNegativePrefix () { return negativePrefix; } public String getNegativeSuffix () { return negativeSuffix; } public String getPositivePrefix () { return positivePrefix; } public String getPositiveSuffix () { return positiveSuffix; } public int hashCode () { int hash = (negativeSuffix.hashCode() ^ negativePrefix.hashCode() ^positivePrefix.hashCode() ^ positiveSuffix.hashCode()); // FIXME. return hash; } public boolean isDecimalSeparatorAlwaysShown () { return decimalSeparatorAlwaysShown; } public Number parse (String str, ParsePosition pos) { // Our strategy is simple: copy the text into a buffer, // translating or omitting locale-specific information. Then // let Double or Long convert the number for us. boolean is_neg = false; int index = pos.getIndex(); StringBuffer buf = new StringBuffer (); // We have to check both prefixes, because one might be empty. // We want to pick the longest prefix that matches. boolean got_pos = str.startsWith(positivePrefix, index); String np = (negativePrefix != null ? negativePrefix : positivePrefix + symbols.getMinusSign()); boolean got_neg = str.startsWith(np, index); if (got_pos && got_neg) { // By checking this way, we preserve ambiguity in the case // where the negative format differs only in suffix. We // check this again later. if (np.length() > positivePrefix.length()) { is_neg = true; index += np.length(); } else index += positivePrefix.length(); } else if (got_neg) { is_neg = true; index += np.length(); } else if (got_pos) index += positivePrefix.length(); else { pos.setErrorIndex (index); return null; } // FIXME: handle Inf and NaN. // FIXME: do we have to respect minimum/maxmimum digit stuff? // What about leading zeros? What about multiplier? int start_index = index; int max = str.length(); char zero = symbols.getZeroDigit(); int last_group = -1; boolean int_part = true; boolean exp_part = false; for (; index < max; ++index) { char c = str.charAt(index); // FIXME: what about grouping size? if (groupingUsed && c == symbols.getGroupingSeparator()) { if (last_group != -1 && (index - last_group) % groupingSize != 0) { pos.setErrorIndex(index); return null; } last_group = index; } else if (c >= zero && c <= zero + 9) { buf.append((char) (c - zero + '0')); exp_part = false; } else if (parseIntegerOnly) break; else if (c == symbols.getDecimalSeparator()) { if (last_group != -1 && (index - last_group) % groupingSize != 0) { pos.setErrorIndex(index); return null; } buf.append('.'); int_part = false; } else if (c == symbols.getExponential()) { buf.append('E'); int_part = false; exp_part = true; } else if (exp_part && (c == '+' || c == '-' || c == symbols.getMinusSign())) { // For exponential notation. buf.append(c); } else break; } if (index == start_index) { // Didn't see any digits. pos.setErrorIndex(index); return null; } // Check the suffix. We must do this before converting the // buffer to a number to handle the case of a number which is // the most negative Long. boolean got_pos_suf = str.startsWith(positiveSuffix, index); String ns = (negativePrefix == null ? positiveSuffix : negativeSuffix); boolean got_neg_suf = str.startsWith(ns, index); if (is_neg) { if (! got_neg_suf) { pos.setErrorIndex(index); return null; } } else if (got_pos && got_neg && got_neg_suf) { is_neg = true; } else if (got_pos != got_pos_suf && got_neg != got_neg_suf) { pos.setErrorIndex(index); return null; } String suffix = is_neg ? ns : positiveSuffix; if (is_neg) buf.insert(0, '-'); String t = buf.toString(); Number result = null; try { result = new Long (t); } catch (NumberFormatException x1) { try { result = new Double (t); } catch (NumberFormatException x2) { } } if (result == null) { pos.setErrorIndex(index); return null; } pos.setIndex(index + suffix.length()); return result; } public void setDecimalFormatSymbols (DecimalFormatSymbols newSymbols) { symbols = newSymbols; } public void setDecimalSeparatorAlwaysShown (boolean newValue) { decimalSeparatorAlwaysShown = newValue; } public void setGroupingSize (int groupSize) { groupingSize = (byte) groupSize; } public void setMaximumFractionDigits (int newValue) { maximumFractionDigits = Math.min(newValue, 340); } public void setMaximumIntegerDigits (int newValue) { maximumIntegerDigits = Math.min(newValue, 309); } public void setMinimumFractionDigits (int newValue) { minimumFractionDigits = Math.min(newValue, 340); } public void setMinimumIntegerDigits (int newValue) { minimumIntegerDigits = Math.min(newValue, 309); } public void setMultiplier (int newValue) { multiplier = newValue; } public void setNegativePrefix (String newValue) { negativePrefix = newValue; } public void setNegativeSuffix (String newValue) { negativeSuffix = newValue; } public void setPositivePrefix (String newValue) { positivePrefix = newValue; } public void setPositiveSuffix (String newValue) { positiveSuffix = newValue; } private final void quoteFix (StringBuffer buf, String text, String patChars) { int len = text.length(); for (int index = 0; index < len; ++index) { char c = text.charAt(index); if (patChars.indexOf(c) != -1) { buf.append('\''); buf.append(c); buf.append('\''); } else buf.append(c); } } private final String computePattern (DecimalFormatSymbols syms) { StringBuffer mainPattern = new StringBuffer (); // We have to at least emit a zero for the minimum number of // digits. Past that we need hash marks up to the grouping // separator (and one beyond). int total_digits = Math.max(minimumIntegerDigits, groupingUsed ? groupingSize + 1: 0); for (int i = 0; i < total_digits - minimumIntegerDigits; ++i) mainPattern.append(syms.getDigit()); for (int i = total_digits - minimumIntegerDigits; i < total_digits; ++i) mainPattern.append(syms.getZeroDigit()); // Inserting the gropuing operator afterwards is easier. if (groupingUsed) mainPattern.insert(mainPattern.length() - groupingSize, syms.getGroupingSeparator()); // See if we need decimal info. if (minimumFractionDigits > 0 || maximumFractionDigits > 0 || decimalSeparatorAlwaysShown) mainPattern.append(syms.getDecimalSeparator()); for (int i = 0; i < minimumFractionDigits; ++i) mainPattern.append(syms.getZeroDigit()); for (int i = minimumFractionDigits; i < maximumFractionDigits; ++i) mainPattern.append(syms.getDigit()); if (useExponentialNotation) { mainPattern.append(syms.getExponential()); for (int i = 0; i < minExponentDigits; ++i) mainPattern.append(syms.getZeroDigit()); if (minExponentDigits == 0) mainPattern.append(syms.getDigit()); } String main = mainPattern.toString(); String patChars = patternChars (syms); mainPattern.setLength(0); quoteFix (mainPattern, positivePrefix, patChars); mainPattern.append(main); quoteFix (mainPattern, positiveSuffix, patChars); if (negativePrefix != null) { quoteFix (mainPattern, negativePrefix, patChars); mainPattern.append(main); quoteFix (mainPattern, negativeSuffix, patChars); } return mainPattern.toString(); } public String toLocalizedPattern () { return computePattern (symbols); } public String toPattern () { return computePattern (nonLocalizedSymbols); } // These names are fixed by the serialization spec. private boolean decimalSeparatorAlwaysShown; private byte groupingSize; private byte minExponentDigits; private int multiplier; private String negativePrefix; private String negativeSuffix; private String positivePrefix; private String positiveSuffix; private DecimalFormatSymbols symbols; private boolean useExponentialNotation; // The locale-independent pattern symbols happen to be the same as // the US symbols. private static final DecimalFormatSymbols nonLocalizedSymbols = new DecimalFormatSymbols (Locale.US); }