summaryrefslogtreecommitdiff
path: root/java/util/Formatter.java
diff options
context:
space:
mode:
Diffstat (limited to 'java/util/Formatter.java')
-rw-r--r--java/util/Formatter.java1294
1 files changed, 1294 insertions, 0 deletions
diff --git a/java/util/Formatter.java b/java/util/Formatter.java
new file mode 100644
index 000000000..01d546315
--- /dev/null
+++ b/java/util/Formatter.java
@@ -0,0 +1,1294 @@
+/* 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 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 StringBuilder 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");
+
+ /**
+ * 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(StringBuilder 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(StringBuilder app, Locale loc)
+ {
+ this.out = app == null ? new StringBuilder() : app;
+ this.locale = 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;
+ 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();
+ }
+
+ /**
+ * 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(StringBuilder 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);
+
+ 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 StringBuilder basicIntegralConversion(Object arg, int flags,
+ int width, int precision,
+ int radix, char conversion)
+ {
+ assert radix == 8 || radix == 10 || radix == 16;
+ noPrecision(precision);
+
+ // Some error checking.
+ if ((flags & FormattableFlags.ZERO) != 0
+ && (flags & FormattableFlags.LEFT_JUSTIFY) == 0)
+ throw new IllegalFormatFlagsException(getName(flags));
+ 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 StringBuilder(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;
+
+ StringBuilder 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
+ {
+ StringBuilder 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(StringBuilder 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);
+
+ StringBuilder result = new StringBuilder();
+ 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.decode(format.substring(start, index)).intValue();
+ }
+
+ /**
+ * 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 (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 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(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 StringBuilder 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();
+ }
+}