| /* 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(); | |
| } | |
| } |