| /* Formatter.java -- printf-style formatting |
| Copyright (C) 2005 Free Software Foundation, Inc. |
| |
| This file is part of GNU Classpath. |
| |
| GNU Classpath is free software; you can redistribute it and/or modify |
| it under the terms of the GNU General Public License as published by |
| the Free Software Foundation; either version 2, or (at your option) |
| any later version. |
| |
| GNU Classpath is distributed in the hope that it will be useful, but |
| WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| General Public License for more details. |
| |
| You should have received a copy of the GNU General Public License |
| along with GNU Classpath; see the file COPYING. If not, write to the |
| Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA |
| 02110-1301 USA. |
| |
| Linking this library statically or dynamically with other modules is |
| making a combined work based on this library. Thus, the terms and |
| conditions of the GNU General Public License cover the whole |
| combination. |
| |
| As a special exception, the copyright holders of this library give you |
| permission to link this library with independent modules to produce an |
| executable, regardless of the license terms of these independent |
| modules, and to copy and distribute the resulting executable under |
| terms of your choice, provided that you also meet, for each linked |
| independent module, the terms and conditions of the license of that |
| module. An independent module is a module which is not derived from |
| or based on this library. If you modify this library, you may extend |
| this exception to your version of the library, but you are not |
| obligated to do so. If you do not wish to do so, delete this |
| exception statement from your version. */ |
| |
| |
| package java.util; |
| |
| import gnu.java.lang.CPStringBuilder; |
| |
| import java.io.Closeable; |
| import java.io.File; |
| import java.io.FileNotFoundException; |
| import java.io.FileOutputStream; |
| import java.io.Flushable; |
| import java.io.IOException; |
| import java.io.OutputStream; |
| import java.io.OutputStreamWriter; |
| import java.io.PrintStream; |
| import java.io.UnsupportedEncodingException; |
| import java.math.BigInteger; |
| import java.text.DateFormatSymbols; |
| import java.text.DecimalFormatSymbols; |
| |
| import gnu.classpath.SystemProperties; |
| |
| /** |
| * <p> |
| * A Java formatter for <code>printf</code>-style format strings, |
| * as seen in the C programming language. This differs from the |
| * C interpretation of such strings by performing much stricter |
| * checking of format specifications and their corresponding |
| * arguments. While unknown conversions will be ignored in C, |
| * and invalid conversions will only produce compiler warnings, |
| * the Java version utilises a full range of run-time exceptions to |
| * handle these cases. The Java version is also more customisable |
| * by virtue of the provision of the {@link Formattable} interface, |
| * which allows an arbitrary class to be formatted by the formatter. |
| * </p> |
| * <p> |
| * The formatter is accessible by more convienient static methods. |
| * For example, streams now have appropriate format methods |
| * (the equivalent of <code>fprintf</code>) as do <code>String</code> |
| * objects (the equivalent of <code>sprintf</code>). |
| * </p> |
| * <p> |
| * <strong>Note</strong>: the formatter is not thread-safe. For |
| * multi-threaded access, external synchronization should be provided. |
| * </p> |
| * |
| * @author Tom Tromey (tromey@redhat.com) |
| * @author Andrew John Hughes (gnu_andrew@member.fsf.org) |
| * @since 1.5 |
| */ |
| public final class Formatter |
| implements Closeable, Flushable |
| { |
| |
| /** |
| * The output of the formatter. |
| */ |
| private Appendable out; |
| |
| /** |
| * The locale used by the formatter. |
| */ |
| private Locale locale; |
| |
| /** |
| * Whether or not the formatter is closed. |
| */ |
| private boolean closed; |
| |
| /** |
| * The last I/O exception thrown by the output stream. |
| */ |
| private IOException ioException; |
| |
| // Some state used when actually formatting. |
| /** |
| * The format string. |
| */ |
| private String format; |
| |
| /** |
| * The current index into the string. |
| */ |
| private int index; |
| |
| /** |
| * The length of the format string. |
| */ |
| private int length; |
| |
| /** |
| * The formatting locale. |
| */ |
| private Locale fmtLocale; |
| |
| // Note that we include '-' twice. The flags are ordered to |
| // correspond to the values in FormattableFlags, and there is no |
| // flag (in the sense of this field used when parsing) for |
| // UPPERCASE; the second '-' serves as a placeholder. |
| /** |
| * A string used to index into the formattable flags. |
| */ |
| private static final String FLAGS = "--#+ 0,("; |
| |
| /** |
| * The system line separator. |
| */ |
| private static final String lineSeparator |
| = SystemProperties.getProperty("line.separator"); |
| |
| /** |
| * The type of numeric output format for a {@link BigDecimal}. |
| */ |
| public enum BigDecimalLayoutForm |
| { |
| DECIMAL_FLOAT, |
| SCIENTIFIC |
| } |
| |
| /** |
| * Constructs a new <code>Formatter</code> using the default |
| * locale and a {@link StringBuilder} as the output stream. |
| */ |
| public Formatter() |
| { |
| this(null, Locale.getDefault()); |
| } |
| |
| /** |
| * Constructs a new <code>Formatter</code> using the specified |
| * locale and a {@link StringBuilder} as the output stream. |
| * If the locale is <code>null</code>, then no localization |
| * is applied. |
| * |
| * @param loc the locale to use. |
| */ |
| public Formatter(Locale loc) |
| { |
| this(null, loc); |
| } |
| |
| /** |
| * Constructs a new <code>Formatter</code> using the default |
| * locale and the specified output stream. |
| * |
| * @param app the output stream to use. |
| */ |
| public Formatter(Appendable app) |
| { |
| this(app, Locale.getDefault()); |
| } |
| |
| /** |
| * Constructs a new <code>Formatter</code> using the specified |
| * locale and the specified output stream. If the locale is |
| * <code>null</code>, then no localization is applied. |
| * |
| * @param app the output stream to use. |
| * @param loc the locale to use. |
| */ |
| public Formatter(Appendable app, Locale loc) |
| { |
| this.out = app == null ? new StringBuilder() : app; |
| this.locale = loc; |
| } |
| |
| /** |
| * Constructs a new <code>Formatter</code> using the default |
| * locale and character set, with the specified file as the |
| * output stream. |
| * |
| * @param file the file to use for output. |
| * @throws FileNotFoundException if the file does not exist |
| * and can not be created. |
| * @throws SecurityException if a security manager is present |
| * and doesn't allow writing to the file. |
| */ |
| public Formatter(File file) |
| throws FileNotFoundException |
| { |
| this(new OutputStreamWriter(new FileOutputStream(file))); |
| } |
| |
| /** |
| * Constructs a new <code>Formatter</code> using the default |
| * locale, with the specified file as the output stream |
| * and the supplied character set. |
| * |
| * @param file the file to use for output. |
| * @param charset the character set to use for output. |
| * @throws FileNotFoundException if the file does not exist |
| * and can not be created. |
| * @throws SecurityException if a security manager is present |
| * and doesn't allow writing to the file. |
| * @throws UnsupportedEncodingException if the supplied character |
| * set is not supported. |
| */ |
| public Formatter(File file, String charset) |
| throws FileNotFoundException, UnsupportedEncodingException |
| { |
| this(file, charset, Locale.getDefault()); |
| } |
| |
| /** |
| * Constructs a new <code>Formatter</code> using the specified |
| * file as the output stream with the supplied character set |
| * and locale. If the locale is <code>null</code>, then no |
| * localization is applied. |
| * |
| * @param file the file to use for output. |
| * @param charset the character set to use for output. |
| * @param loc the locale to use. |
| * @throws FileNotFoundException if the file does not exist |
| * and can not be created. |
| * @throws SecurityException if a security manager is present |
| * and doesn't allow writing to the file. |
| * @throws UnsupportedEncodingException if the supplied character |
| * set is not supported. |
| */ |
| public Formatter(File file, String charset, Locale loc) |
| throws FileNotFoundException, UnsupportedEncodingException |
| { |
| this(new OutputStreamWriter(new FileOutputStream(file), charset), |
| loc); |
| } |
| |
| /** |
| * Constructs a new <code>Formatter</code> using the default |
| * locale and character set, with the specified output stream. |
| * |
| * @param out the output stream to use. |
| */ |
| public Formatter(OutputStream out) |
| { |
| this(new OutputStreamWriter(out)); |
| } |
| |
| /** |
| * Constructs a new <code>Formatter</code> using the default |
| * locale, with the specified file output stream and the |
| * supplied character set. |
| * |
| * @param out the output stream. |
| * @param charset the character set to use for output. |
| * @throws UnsupportedEncodingException if the supplied character |
| * set is not supported. |
| */ |
| public Formatter(OutputStream out, String charset) |
| throws UnsupportedEncodingException |
| { |
| this(out, charset, Locale.getDefault()); |
| } |
| |
| /** |
| * Constructs a new <code>Formatter</code> using the specified |
| * output stream with the supplied character set and locale. |
| * If the locale is <code>null</code>, then no localization is |
| * applied. |
| * |
| * @param out the output stream. |
| * @param charset the character set to use for output. |
| * @param loc the locale to use. |
| * @throws UnsupportedEncodingException if the supplied character |
| * set is not supported. |
| */ |
| public Formatter(OutputStream out, String charset, Locale loc) |
| throws UnsupportedEncodingException |
| { |
| this(new OutputStreamWriter(out, charset), loc); |
| } |
| |
| /** |
| * Constructs a new <code>Formatter</code> using the default |
| * locale with the specified output stream. The character |
| * set used is that of the output stream. |
| * |
| * @param out the output stream to use. |
| */ |
| public Formatter(PrintStream out) |
| { |
| this((Appendable) out); |
| } |
| |
| /** |
| * Constructs a new <code>Formatter</code> using the default |
| * locale and character set, with the specified file as the |
| * output stream. |
| * |
| * @param file the file to use for output. |
| * @throws FileNotFoundException if the file does not exist |
| * and can not be created. |
| * @throws SecurityException if a security manager is present |
| * and doesn't allow writing to the file. |
| */ |
| public Formatter(String file) throws FileNotFoundException |
| { |
| this(new OutputStreamWriter(new FileOutputStream(file))); |
| } |
| |
| /** |
| * Constructs a new <code>Formatter</code> using the default |
| * locale, with the specified file as the output stream |
| * and the supplied character set. |
| * |
| * @param file the file to use for output. |
| * @param charset the character set to use for output. |
| * @throws FileNotFoundException if the file does not exist |
| * and can not be created. |
| * @throws SecurityException if a security manager is present |
| * and doesn't allow writing to the file. |
| * @throws UnsupportedEncodingException if the supplied character |
| * set is not supported. |
| */ |
| public Formatter(String file, String charset) |
| throws FileNotFoundException, UnsupportedEncodingException |
| { |
| this(file, charset, Locale.getDefault()); |
| } |
| |
| /** |
| * Constructs a new <code>Formatter</code> using the specified |
| * file as the output stream with the supplied character set |
| * and locale. If the locale is <code>null</code>, then no |
| * localization is applied. |
| * |
| * @param file the file to use for output. |
| * @param charset the character set to use for output. |
| * @param loc the locale to use. |
| * @throws FileNotFoundException if the file does not exist |
| * and can not be created. |
| * @throws SecurityException if a security manager is present |
| * and doesn't allow writing to the file. |
| * @throws UnsupportedEncodingException if the supplied character |
| * set is not supported. |
| */ |
| public Formatter(String file, String charset, Locale loc) |
| throws FileNotFoundException, UnsupportedEncodingException |
| { |
| this(new OutputStreamWriter(new FileOutputStream(file), charset), |
| loc); |
| } |
| |
| /** |
| * Closes the formatter, so as to release used resources. |
| * If the underlying output stream supports the {@link Closeable} |
| * interface, then this is also closed. Attempts to use |
| * a formatter instance, via any method other than |
| * {@link #ioException()}, after closure results in a |
| * {@link FormatterClosedException}. |
| */ |
| public void close() |
| { |
| if (closed) |
| return; |
| try |
| { |
| if (out instanceof Closeable) |
| ((Closeable) out).close(); |
| } |
| catch (IOException _) |
| { |
| // FIXME: do we ignore these or do we set ioException? |
| // The docs seem to indicate that we should ignore. |
| } |
| closed = true; |
| } |
| |
| /** |
| * Flushes the formatter, writing any cached data to the output |
| * stream. If the underlying output stream supports the |
| * {@link Flushable} interface, it is also flushed. |
| * |
| * @throws FormatterClosedException if the formatter is closed. |
| */ |
| public void flush() |
| { |
| if (closed) |
| throw new FormatterClosedException(); |
| try |
| { |
| if (out instanceof Flushable) |
| ((Flushable) out).flush(); |
| } |
| catch (IOException _) |
| { |
| // FIXME: do we ignore these or do we set ioException? |
| // The docs seem to indicate that we should ignore. |
| } |
| } |
| |
| /** |
| * Return the name corresponding to a flag. |
| * |
| * @param flags the flag to return the name of. |
| * @return the name of the flag. |
| */ |
| private String getName(int flags) |
| { |
| // FIXME: do we want all the flags in here? |
| // Or should we redo how this is reported? |
| int bit = Integer.numberOfTrailingZeros(flags); |
| return FLAGS.substring(bit, bit + 1); |
| } |
| |
| /** |
| * Verify the flags passed to a conversion. |
| * |
| * @param flags the flags to verify. |
| * @param allowed the allowed flags mask. |
| * @param conversion the conversion character. |
| */ |
| private void checkFlags(int flags, int allowed, char conversion) |
| { |
| flags &= ~allowed; |
| if (flags != 0) |
| throw new FormatFlagsConversionMismatchException(getName(flags), |
| conversion); |
| } |
| |
| /** |
| * Throw an exception if a precision was specified. |
| * |
| * @param precision the precision value (-1 indicates not specified). |
| */ |
| private void noPrecision(int precision) |
| { |
| if (precision != -1) |
| throw new IllegalFormatPrecisionException(precision); |
| } |
| |
| /** |
| * Apply the numeric localization algorithm to a StringBuilder. |
| * |
| * @param builder the builder to apply to. |
| * @param flags the formatting flags to use. |
| * @param width the width of the numeric value. |
| * @param isNegative true if the value is negative. |
| */ |
| private void applyLocalization(CPStringBuilder builder, int flags, int width, |
| boolean isNegative) |
| { |
| DecimalFormatSymbols dfsyms; |
| if (fmtLocale == null) |
| dfsyms = new DecimalFormatSymbols(); |
| else |
| dfsyms = new DecimalFormatSymbols(fmtLocale); |
| |
| // First replace each digit. |
| char zeroDigit = dfsyms.getZeroDigit(); |
| int decimalOffset = -1; |
| for (int i = builder.length() - 1; i >= 0; --i) |
| { |
| char c = builder.charAt(i); |
| if (c >= '0' && c <= '9') |
| builder.setCharAt(i, (char) (c - '0' + zeroDigit)); |
| else if (c == '.') |
| { |
| assert decimalOffset == -1; |
| decimalOffset = i; |
| } |
| } |
| |
| // Localize the decimal separator. |
| if (decimalOffset != -1) |
| { |
| builder.deleteCharAt(decimalOffset); |
| builder.insert(decimalOffset, dfsyms.getDecimalSeparator()); |
| } |
| |
| // Insert the grouping separators. |
| if ((flags & FormattableFlags.COMMA) != 0) |
| { |
| char groupSeparator = dfsyms.getGroupingSeparator(); |
| int groupSize = 3; // FIXME |
| int offset = (decimalOffset == -1) ? builder.length() : decimalOffset; |
| // We use '>' because we don't want to insert a separator |
| // before the first digit. |
| for (int i = offset - groupSize; i > 0; i -= groupSize) |
| builder.insert(i, groupSeparator); |
| } |
| |
| if ((flags & FormattableFlags.ZERO) != 0) |
| { |
| // Zero fill. Note that according to the algorithm we do not |
| // insert grouping separators here. |
| for (int i = width - builder.length(); i > 0; --i) |
| builder.insert(0, zeroDigit); |
| } |
| |
| if (isNegative) |
| { |
| if ((flags & FormattableFlags.PAREN) != 0) |
| { |
| builder.insert(0, '('); |
| builder.append(')'); |
| } |
| else |
| builder.insert(0, '-'); |
| } |
| else if ((flags & FormattableFlags.PLUS) != 0) |
| builder.insert(0, '+'); |
| else if ((flags & FormattableFlags.SPACE) != 0) |
| builder.insert(0, ' '); |
| } |
| |
| /** |
| * A helper method that handles emitting a String after applying |
| * precision, width, justification, and upper case flags. |
| * |
| * @param arg the string to emit. |
| * @param flags the formatting flags to use. |
| * @param width the width to use. |
| * @param precision the precision to use. |
| * @throws IOException if the output stream throws an I/O error. |
| */ |
| private void genericFormat(String arg, int flags, int width, int precision) |
| throws IOException |
| { |
| if ((flags & FormattableFlags.UPPERCASE) != 0) |
| { |
| if (fmtLocale == null) |
| arg = arg.toUpperCase(); |
| else |
| arg = arg.toUpperCase(fmtLocale); |
| } |
| |
| if (precision >= 0 && arg.length() > precision) |
| arg = arg.substring(0, precision); |
| |
| boolean leftJustify = (flags & FormattableFlags.LEFT_JUSTIFY) != 0; |
| if (leftJustify && width == -1) |
| throw new MissingFormatWidthException("fixme"); |
| if (! leftJustify && arg.length() < width) |
| { |
| for (int i = width - arg.length(); i > 0; --i) |
| out.append(' '); |
| } |
| out.append(arg); |
| if (leftJustify && arg.length() < width) |
| { |
| for (int i = width - arg.length(); i > 0; --i) |
| out.append(' '); |
| } |
| } |
| |
| /** |
| * Emit a boolean. |
| * |
| * @param arg the boolean to emit. |
| * @param flags the formatting flags to use. |
| * @param width the width to use. |
| * @param precision the precision to use. |
| * @param conversion the conversion character. |
| * @throws IOException if the output stream throws an I/O error. |
| */ |
| private void booleanFormat(Object arg, int flags, int width, int precision, |
| char conversion) |
| throws IOException |
| { |
| checkFlags(flags, |
| FormattableFlags.LEFT_JUSTIFY | FormattableFlags.UPPERCASE, |
| conversion); |
| String result; |
| if (arg instanceof Boolean) |
| result = String.valueOf((Boolean) arg); |
| else |
| result = arg == null ? "false" : "true"; |
| genericFormat(result, flags, width, precision); |
| } |
| |
| /** |
| * Emit a hash code. |
| * |
| * @param arg the hash code to emit. |
| * @param flags the formatting flags to use. |
| * @param width the width to use. |
| * @param precision the precision to use. |
| * @param conversion the conversion character. |
| * @throws IOException if the output stream throws an I/O error. |
| */ |
| private void hashCodeFormat(Object arg, int flags, int width, int precision, |
| char conversion) |
| throws IOException |
| { |
| checkFlags(flags, |
| FormattableFlags.LEFT_JUSTIFY | FormattableFlags.UPPERCASE, |
| conversion); |
| genericFormat(arg == null ? "null" : Integer.toHexString(arg.hashCode()), |
| flags, width, precision); |
| } |
| |
| /** |
| * Emit a String or Formattable conversion. |
| * |
| * @param arg the String or Formattable to emit. |
| * @param flags the formatting flags to use. |
| * @param width the width to use. |
| * @param precision the precision to use. |
| * @param conversion the conversion character. |
| * @throws IOException if the output stream throws an I/O error. |
| */ |
| private void stringFormat(Object arg, int flags, int width, int precision, |
| char conversion) |
| throws IOException |
| { |
| if (arg instanceof Formattable) |
| { |
| checkFlags(flags, |
| (FormattableFlags.LEFT_JUSTIFY |
| | FormattableFlags.UPPERCASE |
| | FormattableFlags.ALTERNATE), |
| conversion); |
| Formattable fmt = (Formattable) arg; |
| fmt.formatTo(this, flags, width, precision); |
| } |
| else |
| { |
| checkFlags(flags, |
| FormattableFlags.LEFT_JUSTIFY | FormattableFlags.UPPERCASE, |
| conversion); |
| genericFormat(arg == null ? "null" : arg.toString(), flags, width, |
| precision); |
| } |
| } |
| |
| /** |
| * Emit a character. |
| * |
| * @param arg the character to emit. |
| * @param flags the formatting flags to use. |
| * @param width the width to use. |
| * @param precision the precision to use. |
| * @param conversion the conversion character. |
| * @throws IOException if the output stream throws an I/O error. |
| */ |
| private void characterFormat(Object arg, int flags, int width, int precision, |
| char conversion) |
| throws IOException |
| { |
| checkFlags(flags, |
| FormattableFlags.LEFT_JUSTIFY | FormattableFlags.UPPERCASE, |
| conversion); |
| noPrecision(precision); |
| |
| if (arg == null) |
| { |
| genericFormat("null", flags, width, precision); |
| return; |
| } |
| |
| int theChar; |
| if (arg instanceof Character) |
| theChar = ((Character) arg).charValue(); |
| else if (arg instanceof Byte) |
| theChar = (char) (((Byte) arg).byteValue ()); |
| else if (arg instanceof Short) |
| theChar = (char) (((Short) arg).shortValue ()); |
| else if (arg instanceof Integer) |
| { |
| theChar = ((Integer) arg).intValue(); |
| if (! Character.isValidCodePoint(theChar)) |
| throw new IllegalFormatCodePointException(theChar); |
| } |
| else |
| throw new IllegalFormatConversionException(conversion, arg.getClass()); |
| String result = new String(Character.toChars(theChar)); |
| genericFormat(result, flags, width, precision); |
| } |
| |
| /** |
| * Emit a '%'. |
| * |
| * @param flags the formatting flags to use. |
| * @param width the width to use. |
| * @param precision the precision to use. |
| * @throws IOException if the output stream throws an I/O error. |
| */ |
| private void percentFormat(int flags, int width, int precision) |
| throws IOException |
| { |
| checkFlags(flags, FormattableFlags.LEFT_JUSTIFY, '%'); |
| noPrecision(precision); |
| genericFormat("%", flags, width, precision); |
| } |
| |
| /** |
| * Emit a newline. |
| * |
| * @param flags the formatting flags to use. |
| * @param width the width to use. |
| * @param precision the precision to use. |
| * @throws IOException if the output stream throws an I/O error. |
| */ |
| private void newLineFormat(int flags, int width, int precision) |
| throws IOException |
| { |
| checkFlags(flags, 0, 'n'); |
| noPrecision(precision); |
| if (width != -1) |
| throw new IllegalFormatWidthException(width); |
| genericFormat(lineSeparator, flags, width, precision); |
| } |
| |
| /** |
| * Helper method to do initial formatting and checking for integral |
| * conversions. |
| * |
| * @param arg the formatted argument. |
| * @param flags the formatting flags to use. |
| * @param width the width to use. |
| * @param precision the precision to use. |
| * @param radix the radix of the number. |
| * @param conversion the conversion character. |
| * @return the result. |
| */ |
| private CPStringBuilder basicIntegralConversion(Object arg, int flags, |
| int width, int precision, |
| int radix, char conversion) |
| { |
| assert radix == 8 || radix == 10 || radix == 16; |
| |
| if (arg == null) |
| { |
| return new CPStringBuilder("null"); |
| } |
| |
| noPrecision(precision); |
| |
| // Some error checking. |
| if ((flags & FormattableFlags.PLUS) != 0 |
| && (flags & FormattableFlags.SPACE) != 0) |
| throw new IllegalFormatFlagsException(getName(flags)); |
| |
| if ((flags & FormattableFlags.LEFT_JUSTIFY) != 0 && width == -1) |
| throw new MissingFormatWidthException("fixme"); |
| |
| // Do the base translation of the value to a string. |
| String result; |
| int basicFlags = (FormattableFlags.LEFT_JUSTIFY |
| // We already handled any possible error when |
| // parsing. |
| | FormattableFlags.UPPERCASE |
| | FormattableFlags.ZERO); |
| if (radix == 10) |
| basicFlags |= (FormattableFlags.PLUS |
| | FormattableFlags.SPACE |
| | FormattableFlags.COMMA |
| | FormattableFlags.PAREN); |
| else |
| basicFlags |= FormattableFlags.ALTERNATE; |
| |
| if (arg instanceof BigInteger) |
| { |
| checkFlags(flags, |
| (basicFlags |
| | FormattableFlags.PLUS |
| | FormattableFlags.SPACE |
| | FormattableFlags.PAREN), |
| conversion); |
| BigInteger bi = (BigInteger) arg; |
| result = bi.toString(radix); |
| } |
| else if (arg instanceof Number |
| && ! (arg instanceof Float) |
| && ! (arg instanceof Double)) |
| { |
| checkFlags(flags, basicFlags, conversion); |
| long value = ((Number) arg).longValue (); |
| if (radix == 8) |
| result = Long.toOctalString(value); |
| else if (radix == 16) |
| result = Long.toHexString(value); |
| else |
| result = Long.toString(value); |
| } |
| else |
| throw new IllegalFormatConversionException(conversion, arg.getClass()); |
| |
| return new CPStringBuilder(result); |
| } |
| |
| /** |
| * Emit a hex or octal value. |
| * |
| * @param arg the hexadecimal or octal value. |
| * @param flags the formatting flags to use. |
| * @param width the width to use. |
| * @param precision the precision to use. |
| * @param radix the radix of the number. |
| * @param conversion the conversion character. |
| * @throws IOException if the output stream throws an I/O error. |
| */ |
| private void hexOrOctalConversion(Object arg, int flags, int width, |
| int precision, int radix, |
| char conversion) |
| throws IOException |
| { |
| assert radix == 8 || radix == 16; |
| |
| CPStringBuilder builder = basicIntegralConversion(arg, flags, width, |
| precision, radix, |
| conversion); |
| int insertPoint = 0; |
| |
| // Insert the sign. |
| if (builder.charAt(0) == '-') |
| { |
| // Already inserted. Note that we don't insert a sign, since |
| // the only case where it is needed it BigInteger, and it has |
| // already been inserted by toString. |
| ++insertPoint; |
| } |
| else if ((flags & FormattableFlags.PLUS) != 0) |
| { |
| builder.insert(insertPoint, '+'); |
| ++insertPoint; |
| } |
| else if ((flags & FormattableFlags.SPACE) != 0) |
| { |
| builder.insert(insertPoint, ' '); |
| ++insertPoint; |
| } |
| |
| // Insert the radix prefix. |
| if ((flags & FormattableFlags.ALTERNATE) != 0) |
| { |
| builder.insert(insertPoint, radix == 8 ? "0" : "0x"); |
| insertPoint += radix == 8 ? 1 : 2; |
| } |
| |
| // Now justify the result. |
| int resultWidth = builder.length(); |
| if (resultWidth < width) |
| { |
| char fill = ((flags & FormattableFlags.ZERO) != 0) ? '0' : ' '; |
| if ((flags & FormattableFlags.LEFT_JUSTIFY) != 0) |
| { |
| // Left justify. |
| if (fill == ' ') |
| insertPoint = builder.length(); |
| } |
| else |
| { |
| // Right justify. Insert spaces before the radix prefix |
| // and sign. |
| insertPoint = 0; |
| } |
| while (resultWidth++ < width) |
| builder.insert(insertPoint, fill); |
| } |
| |
| String result = builder.toString(); |
| if ((flags & FormattableFlags.UPPERCASE) != 0) |
| { |
| if (fmtLocale == null) |
| result = result.toUpperCase(); |
| else |
| result = result.toUpperCase(fmtLocale); |
| } |
| |
| out.append(result); |
| } |
| |
| /** |
| * Emit a decimal value. |
| * |
| * @param arg the hexadecimal or octal value. |
| * @param flags the formatting flags to use. |
| * @param width the width to use. |
| * @param precision the precision to use. |
| * @param conversion the conversion character. |
| * @throws IOException if the output stream throws an I/O error. |
| */ |
| private void decimalConversion(Object arg, int flags, int width, |
| int precision, char conversion) |
| throws IOException |
| { |
| CPStringBuilder builder = basicIntegralConversion(arg, flags, width, |
| precision, 10, |
| conversion); |
| boolean isNegative = false; |
| if (builder.charAt(0) == '-') |
| { |
| // Sign handling is done during localization. |
| builder.deleteCharAt(0); |
| isNegative = true; |
| } |
| |
| applyLocalization(builder, flags, width, isNegative); |
| genericFormat(builder.toString(), flags, width, precision); |
| } |
| |
| /** |
| * Emit a single date or time conversion to a StringBuilder. |
| * |
| * @param builder the builder to write to. |
| * @param cal the calendar to use in the conversion. |
| * @param conversion the formatting character to specify the type of data. |
| * @param syms the date formatting symbols. |
| */ |
| private void singleDateTimeConversion(CPStringBuilder builder, Calendar cal, |
| char conversion, |
| DateFormatSymbols syms) |
| { |
| int oldLen = builder.length(); |
| int digits = -1; |
| switch (conversion) |
| { |
| case 'H': |
| builder.append(cal.get(Calendar.HOUR_OF_DAY)); |
| digits = 2; |
| break; |
| case 'I': |
| builder.append(cal.get(Calendar.HOUR)); |
| digits = 2; |
| break; |
| case 'k': |
| builder.append(cal.get(Calendar.HOUR_OF_DAY)); |
| break; |
| case 'l': |
| builder.append(cal.get(Calendar.HOUR)); |
| break; |
| case 'M': |
| builder.append(cal.get(Calendar.MINUTE)); |
| digits = 2; |
| break; |
| case 'S': |
| builder.append(cal.get(Calendar.SECOND)); |
| digits = 2; |
| break; |
| case 'N': |
| // FIXME: nanosecond ... |
| digits = 9; |
| break; |
| case 'p': |
| { |
| int ampm = cal.get(Calendar.AM_PM); |
| builder.append(syms.getAmPmStrings()[ampm]); |
| } |
| break; |
| case 'z': |
| { |
| int zone = cal.get(Calendar.ZONE_OFFSET) / (1000 * 60); |
| builder.append(zone); |
| digits = 4; |
| // Skip the '-' sign. |
| if (zone < 0) |
| ++oldLen; |
| } |
| break; |
| case 'Z': |
| { |
| // FIXME: DST? |
| int zone = cal.get(Calendar.ZONE_OFFSET) / (1000 * 60 * 60); |
| String[][] zs = syms.getZoneStrings(); |
| builder.append(zs[zone + 12][1]); |
| } |
| break; |
| case 's': |
| { |
| long val = cal.getTime().getTime(); |
| builder.append(val / 1000); |
| } |
| break; |
| case 'Q': |
| { |
| long val = cal.getTime().getTime(); |
| builder.append(val); |
| } |
| break; |
| case 'B': |
| { |
| int month = cal.get(Calendar.MONTH); |
| builder.append(syms.getMonths()[month]); |
| } |
| break; |
| case 'b': |
| case 'h': |
| { |
| int month = cal.get(Calendar.MONTH); |
| builder.append(syms.getShortMonths()[month]); |
| } |
| break; |
| case 'A': |
| { |
| int day = cal.get(Calendar.DAY_OF_WEEK); |
| builder.append(syms.getWeekdays()[day]); |
| } |
| break; |
| case 'a': |
| { |
| int day = cal.get(Calendar.DAY_OF_WEEK); |
| builder.append(syms.getShortWeekdays()[day]); |
| } |
| break; |
| case 'C': |
| builder.append(cal.get(Calendar.YEAR) / 100); |
| digits = 2; |
| break; |
| case 'Y': |
| builder.append(cal.get(Calendar.YEAR)); |
| digits = 4; |
| break; |
| case 'y': |
| builder.append(cal.get(Calendar.YEAR) % 100); |
| digits = 2; |
| break; |
| case 'j': |
| builder.append(cal.get(Calendar.DAY_OF_YEAR)); |
| digits = 3; |
| break; |
| case 'm': |
| builder.append(cal.get(Calendar.MONTH) + 1); |
| digits = 2; |
| break; |
| case 'd': |
| builder.append(cal.get(Calendar.DAY_OF_MONTH)); |
| digits = 2; |
| break; |
| case 'e': |
| builder.append(cal.get(Calendar.DAY_OF_MONTH)); |
| break; |
| case 'R': |
| singleDateTimeConversion(builder, cal, 'H', syms); |
| builder.append(':'); |
| singleDateTimeConversion(builder, cal, 'M', syms); |
| break; |
| case 'T': |
| singleDateTimeConversion(builder, cal, 'H', syms); |
| builder.append(':'); |
| singleDateTimeConversion(builder, cal, 'M', syms); |
| builder.append(':'); |
| singleDateTimeConversion(builder, cal, 'S', syms); |
| break; |
| case 'r': |
| singleDateTimeConversion(builder, cal, 'I', syms); |
| builder.append(':'); |
| singleDateTimeConversion(builder, cal, 'M', syms); |
| builder.append(':'); |
| singleDateTimeConversion(builder, cal, 'S', syms); |
| builder.append(' '); |
| singleDateTimeConversion(builder, cal, 'p', syms); |
| break; |
| case 'D': |
| singleDateTimeConversion(builder, cal, 'm', syms); |
| builder.append('/'); |
| singleDateTimeConversion(builder, cal, 'd', syms); |
| builder.append('/'); |
| singleDateTimeConversion(builder, cal, 'y', syms); |
| break; |
| case 'F': |
| singleDateTimeConversion(builder, cal, 'Y', syms); |
| builder.append('-'); |
| singleDateTimeConversion(builder, cal, 'm', syms); |
| builder.append('-'); |
| singleDateTimeConversion(builder, cal, 'd', syms); |
| break; |
| case 'c': |
| singleDateTimeConversion(builder, cal, 'a', syms); |
| builder.append(' '); |
| singleDateTimeConversion(builder, cal, 'b', syms); |
| builder.append(' '); |
| singleDateTimeConversion(builder, cal, 'd', syms); |
| builder.append(' '); |
| singleDateTimeConversion(builder, cal, 'T', syms); |
| builder.append(' '); |
| singleDateTimeConversion(builder, cal, 'Z', syms); |
| builder.append(' '); |
| singleDateTimeConversion(builder, cal, 'Y', syms); |
| break; |
| default: |
| throw new UnknownFormatConversionException(String.valueOf(conversion)); |
| } |
| |
| if (digits > 0) |
| { |
| int newLen = builder.length(); |
| int delta = newLen - oldLen; |
| while (delta++ < digits) |
| builder.insert(oldLen, '0'); |
| } |
| } |
| |
| /** |
| * Emit a date or time value. |
| * |
| * @param arg the date or time value. |
| * @param flags the formatting flags to use. |
| * @param width the width to use. |
| * @param precision the precision to use. |
| * @param conversion the conversion character. |
| * @param subConversion the sub conversion character. |
| * @throws IOException if the output stream throws an I/O error. |
| */ |
| private void dateTimeConversion(Object arg, int flags, int width, |
| int precision, char conversion, |
| char subConversion) |
| throws IOException |
| { |
| noPrecision(precision); |
| checkFlags(flags, |
| FormattableFlags.LEFT_JUSTIFY | FormattableFlags.UPPERCASE, |
| conversion); |
| |
| Calendar cal; |
| if (arg instanceof Calendar) |
| cal = (Calendar) arg; |
| else |
| { |
| Date date; |
| if (arg instanceof Date) |
| date = (Date) arg; |
| else if (arg instanceof Long) |
| date = new Date(((Long) arg).longValue()); |
| else |
| throw new IllegalFormatConversionException(conversion, |
| arg.getClass()); |
| if (fmtLocale == null) |
| cal = Calendar.getInstance(); |
| else |
| cal = Calendar.getInstance(fmtLocale); |
| cal.setTime(date); |
| } |
| |
| // We could try to be more efficient by computing this lazily. |
| DateFormatSymbols syms; |
| if (fmtLocale == null) |
| syms = new DateFormatSymbols(); |
| else |
| syms = new DateFormatSymbols(fmtLocale); |
| |
| CPStringBuilder result = new CPStringBuilder(); |
| singleDateTimeConversion(result, cal, subConversion, syms); |
| |
| genericFormat(result.toString(), flags, width, precision); |
| } |
| |
| /** |
| * Advance the internal parsing index, and throw an exception |
| * on overrun. |
| * |
| * @throws IllegalArgumentException on overrun. |
| */ |
| private void advance() |
| { |
| ++index; |
| if (index >= length) |
| { |
| // FIXME: what exception here? |
| throw new IllegalArgumentException(); |
| } |
| } |
| |
| /** |
| * Parse an integer appearing in the format string. Will return -1 |
| * if no integer was found. |
| * |
| * @return the parsed integer. |
| */ |
| private int parseInt() |
| { |
| int start = index; |
| while (Character.isDigit(format.charAt(index))) |
| advance(); |
| if (start == index) |
| return -1; |
| return Integer.parseInt(format.substring(start, index)); |
| } |
| |
| /** |
| * Parse the argument index. Returns -1 if there was no index, 0 if |
| * we should re-use the previous index, and a positive integer to |
| * indicate an absolute index. |
| * |
| * @return the parsed argument index. |
| */ |
| private int parseArgumentIndex() |
| { |
| int result = -1; |
| int start = index; |
| if (format.charAt(index) == '<') |
| { |
| result = 0; |
| advance(); |
| } |
| else if (Character.isDigit(format.charAt(index))) |
| { |
| result = parseInt(); |
| if (format.charAt(index) == '$') |
| advance(); |
| else |
| { |
| // Reset. |
| index = start; |
| result = -1; |
| } |
| } |
| return result; |
| } |
| |
| /** |
| * Parse a set of flags and return a bit mask of values from |
| * FormattableFlags. Will throw an exception if a flag is |
| * duplicated. |
| * |
| * @return the parsed flags. |
| */ |
| private int parseFlags() |
| { |
| int value = 0; |
| int start = index; |
| while (true) |
| { |
| int x = FLAGS.indexOf(format.charAt(index)); |
| if (x == -1) |
| break; |
| int newValue = 1 << x; |
| if ((value & newValue) != 0) |
| throw new DuplicateFormatFlagsException(format.substring(start, |
| index + 1)); |
| value |= newValue; |
| advance(); |
| } |
| return value; |
| } |
| |
| /** |
| * Parse the width part of a format string. Returns -1 if no width |
| * was specified. |
| * |
| * @return the parsed width. |
| */ |
| private int parseWidth() |
| { |
| return parseInt(); |
| } |
| |
| /** |
| * If the current character is '.', parses the precision part of a |
| * format string. Returns -1 if no precision was specified. |
| * |
| * @return the parsed precision. |
| */ |
| private int parsePrecision() |
| { |
| if (format.charAt(index) != '.') |
| return -1; |
| advance(); |
| int precision = parseInt(); |
| if (precision == -1) |
| // FIXME |
| throw new IllegalArgumentException(); |
| return precision; |
| } |
| |
| /** |
| * Outputs a formatted string based on the supplied specification, |
| * <code>fmt</code>, and its arguments using the specified locale. |
| * The locale of the formatter does not change as a result; the |
| * specified locale is just used for this particular formatting |
| * operation. If the locale is <code>null</code>, then no |
| * localization is applied. |
| * |
| * @param loc the locale to use for this format. |
| * @param fmt the format specification. |
| * @param args the arguments to apply to the specification. |
| * @throws IllegalFormatException if there is a problem with |
| * the syntax of the format |
| * specification or a mismatch |
| * between it and the arguments. |
| * @throws FormatterClosedException if the formatter is closed. |
| */ |
| public Formatter format(Locale loc, String fmt, Object... args) |
| { |
| if (closed) |
| throw new FormatterClosedException(); |
| |
| // Note the arguments are indexed starting at 1. |
| int implicitArgumentIndex = 1; |
| int previousArgumentIndex = 0; |
| |
| try |
| { |
| fmtLocale = loc; |
| format = fmt; |
| length = format.length(); |
| for (index = 0; index < length; ++index) |
| { |
| char c = format.charAt(index); |
| if (c != '%') |
| { |
| out.append(c); |
| continue; |
| } |
| |
| int start = index; |
| advance(); |
| |
| // We do the needed post-processing of this later, when we |
| // determine whether an argument is actually needed by |
| // this conversion. |
| int argumentIndex = parseArgumentIndex(); |
| |
| int flags = parseFlags(); |
| int width = parseWidth(); |
| int precision = parsePrecision(); |
| char origConversion = format.charAt(index); |
| char conversion = origConversion; |
| if (Character.isUpperCase(conversion)) |
| { |
| flags |= FormattableFlags.UPPERCASE; |
| conversion = Character.toLowerCase(conversion); |
| } |
| |
| Object argument = null; |
| if (conversion == '%' || conversion == 'n') |
| { |
| if (argumentIndex != -1) |
| { |
| // FIXME: not sure about this. |
| throw new UnknownFormatConversionException("FIXME"); |
| } |
| } |
| else |
| { |
| if (argumentIndex == -1) |
| argumentIndex = implicitArgumentIndex++; |
| else if (argumentIndex == 0) |
| argumentIndex = previousArgumentIndex; |
| // Argument indices start at 1 but array indices at 0. |
| --argumentIndex; |
| if (args != null) |
| { |
| if (argumentIndex < 0 || argumentIndex >= args.length) |
| throw new MissingFormatArgumentException(format.substring(start, index)); |
| argument = args[argumentIndex]; |
| } |
| } |
| |
| switch (conversion) |
| { |
| case 'b': |
| booleanFormat(argument, flags, width, precision, |
| origConversion); |
| break; |
| case 'h': |
| hashCodeFormat(argument, flags, width, precision, |
| origConversion); |
| break; |
| case 's': |
| stringFormat(argument, flags, width, precision, |
| origConversion); |
| break; |
| case 'c': |
| characterFormat(argument, flags, width, precision, |
| origConversion); |
| break; |
| case 'd': |
| checkFlags(flags & FormattableFlags.UPPERCASE, 0, 'd'); |
| decimalConversion(argument, flags, width, precision, |
| origConversion); |
| break; |
| case 'o': |
| checkFlags(flags & FormattableFlags.UPPERCASE, 0, 'o'); |
| hexOrOctalConversion(argument, flags, width, precision, 8, |
| origConversion); |
| break; |
| case 'x': |
| hexOrOctalConversion(argument, flags, width, precision, 16, |
| origConversion); |
| case 'e': |
| // scientificNotationConversion(); |
| break; |
| case 'f': |
| // floatingDecimalConversion(); |
| break; |
| case 'g': |
| // smartFloatingConversion(); |
| break; |
| case 'a': |
| // hexFloatingConversion(); |
| break; |
| case 't': |
| advance(); |
| char subConversion = format.charAt(index); |
| dateTimeConversion(argument, flags, width, precision, |
| origConversion, subConversion); |
| break; |
| case '%': |
| percentFormat(flags, width, precision); |
| break; |
| case 'n': |
| newLineFormat(flags, width, precision); |
| break; |
| default: |
| throw new UnknownFormatConversionException(String.valueOf(origConversion)); |
| } |
| } |
| } |
| catch (IOException exc) |
| { |
| ioException = exc; |
| } |
| return this; |
| } |
| |
| /** |
| * Outputs a formatted string based on the supplied specification, |
| * <code>fmt</code>, and its arguments using the formatter's locale. |
| * |
| * @param format the format specification. |
| * @param args the arguments to apply to the specification. |
| * @throws IllegalFormatException if there is a problem with |
| * the syntax of the format |
| * specification or a mismatch |
| * between it and the arguments. |
| * @throws FormatterClosedException if the formatter is closed. |
| */ |
| public Formatter format(String format, Object... args) |
| { |
| return format(locale, format, args); |
| } |
| |
| /** |
| * Returns the last I/O exception thrown by the |
| * <code>append()</code> operation of the underlying |
| * output stream. |
| * |
| * @return the last I/O exception. |
| */ |
| public IOException ioException() |
| { |
| return ioException; |
| } |
| |
| /** |
| * Returns the locale used by this formatter. |
| * |
| * @return the formatter's locale. |
| * @throws FormatterClosedException if the formatter is closed. |
| */ |
| public Locale locale() |
| { |
| if (closed) |
| throw new FormatterClosedException(); |
| return locale; |
| } |
| |
| /** |
| * Returns the output stream used by this formatter. |
| * |
| * @return the formatter's output stream. |
| * @throws FormatterClosedException if the formatter is closed. |
| */ |
| public Appendable out() |
| { |
| if (closed) |
| throw new FormatterClosedException(); |
| return out; |
| } |
| |
| /** |
| * Returns the result of applying {@link Object#toString()} |
| * to the underlying output stream. The results returned |
| * depend on the particular {@link Appendable} being used. |
| * For example, a {@link StringBuilder} will return the |
| * formatted output but an I/O stream will not. |
| * |
| * @throws FormatterClosedException if the formatter is closed. |
| */ |
| public String toString() |
| { |
| if (closed) |
| throw new FormatterClosedException(); |
| return out.toString(); |
| } |
| } |