001 /* Formatter.java -- printf-style formatting
002 Copyright (C) 2005 Free Software Foundation, Inc.
003
004 This file is part of GNU Classpath.
005
006 GNU Classpath is free software; you can redistribute it and/or modify
007 it under the terms of the GNU General Public License as published by
008 the Free Software Foundation; either version 2, or (at your option)
009 any later version.
010
011 GNU Classpath is distributed in the hope that it will be useful, but
012 WITHOUT ANY WARRANTY; without even the implied warranty of
013 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
014 General Public License for more details.
015
016 You should have received a copy of the GNU General Public License
017 along with GNU Classpath; see the file COPYING. If not, write to the
018 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
019 02110-1301 USA.
020
021 Linking this library statically or dynamically with other modules is
022 making a combined work based on this library. Thus, the terms and
023 conditions of the GNU General Public License cover the whole
024 combination.
025
026 As a special exception, the copyright holders of this library give you
027 permission to link this library with independent modules to produce an
028 executable, regardless of the license terms of these independent
029 modules, and to copy and distribute the resulting executable under
030 terms of your choice, provided that you also meet, for each linked
031 independent module, the terms and conditions of the license of that
032 module. An independent module is a module which is not derived from
033 or based on this library. If you modify this library, you may extend
034 this exception to your version of the library, but you are not
035 obligated to do so. If you do not wish to do so, delete this
036 exception statement from your version. */
037
038
039 package java.util;
040
041 import java.io.Closeable;
042 import java.io.File;
043 import java.io.FileNotFoundException;
044 import java.io.FileOutputStream;
045 import java.io.Flushable;
046 import java.io.IOException;
047 import java.io.OutputStream;
048 import java.io.OutputStreamWriter;
049 import java.io.PrintStream;
050 import java.io.UnsupportedEncodingException;
051 import java.math.BigInteger;
052 import java.text.DateFormatSymbols;
053 import java.text.DecimalFormatSymbols;
054
055 import gnu.classpath.SystemProperties;
056
057 /**
058 * <p>
059 * A Java formatter for <code>printf</code>-style format strings,
060 * as seen in the C programming language. This differs from the
061 * C interpretation of such strings by performing much stricter
062 * checking of format specifications and their corresponding
063 * arguments. While unknown conversions will be ignored in C,
064 * and invalid conversions will only produce compiler warnings,
065 * the Java version utilises a full range of run-time exceptions to
066 * handle these cases. The Java version is also more customisable
067 * by virtue of the provision of the {@link Formattable} interface,
068 * which allows an arbitrary class to be formatted by the formatter.
069 * </p>
070 * <p>
071 * The formatter is accessible by more convienient static methods.
072 * For example, streams now have appropriate format methods
073 * (the equivalent of <code>fprintf</code>) as do <code>String</code>
074 * objects (the equivalent of <code>sprintf</code>).
075 * </p>
076 * <p>
077 * <strong>Note</strong>: the formatter is not thread-safe. For
078 * multi-threaded access, external synchronization should be provided.
079 * </p>
080 *
081 * @author Tom Tromey (tromey@redhat.com)
082 * @author Andrew John Hughes (gnu_andrew@member.fsf.org)
083 * @since 1.5
084 */
085 public final class Formatter
086 implements Closeable, Flushable
087 {
088
089 /**
090 * The output of the formatter.
091 */
092 private Appendable out;
093
094 /**
095 * The locale used by the formatter.
096 */
097 private Locale locale;
098
099 /**
100 * Whether or not the formatter is closed.
101 */
102 private boolean closed;
103
104 /**
105 * The last I/O exception thrown by the output stream.
106 */
107 private IOException ioException;
108
109 // Some state used when actually formatting.
110 /**
111 * The format string.
112 */
113 private String format;
114
115 /**
116 * The current index into the string.
117 */
118 private int index;
119
120 /**
121 * The length of the format string.
122 */
123 private int length;
124
125 /**
126 * The formatting locale.
127 */
128 private Locale fmtLocale;
129
130 // Note that we include '-' twice. The flags are ordered to
131 // correspond to the values in FormattableFlags, and there is no
132 // flag (in the sense of this field used when parsing) for
133 // UPPERCASE; the second '-' serves as a placeholder.
134 /**
135 * A string used to index into the formattable flags.
136 */
137 private static final String FLAGS = "--#+ 0,(";
138
139 /**
140 * The system line separator.
141 */
142 private static final String lineSeparator
143 = SystemProperties.getProperty("line.separator");
144
145 /**
146 * The type of numeric output format for a {@link BigDecimal}.
147 */
148 public enum BigDecimalLayoutForm
149 {
150 DECIMAL_FLOAT,
151 SCIENTIFIC
152 }
153
154 /**
155 * Constructs a new <code>Formatter</code> using the default
156 * locale and a {@link StringBuilder} as the output stream.
157 */
158 public Formatter()
159 {
160 this(null, Locale.getDefault());
161 }
162
163 /**
164 * Constructs a new <code>Formatter</code> using the specified
165 * locale and a {@link StringBuilder} as the output stream.
166 * If the locale is <code>null</code>, then no localization
167 * is applied.
168 *
169 * @param loc the locale to use.
170 */
171 public Formatter(Locale loc)
172 {
173 this(null, loc);
174 }
175
176 /**
177 * Constructs a new <code>Formatter</code> using the default
178 * locale and the specified output stream.
179 *
180 * @param app the output stream to use.
181 */
182 public Formatter(Appendable app)
183 {
184 this(app, Locale.getDefault());
185 }
186
187 /**
188 * Constructs a new <code>Formatter</code> using the specified
189 * locale and the specified output stream. If the locale is
190 * <code>null</code>, then no localization is applied.
191 *
192 * @param app the output stream to use.
193 * @param loc the locale to use.
194 */
195 public Formatter(Appendable app, Locale loc)
196 {
197 this.out = app == null ? new StringBuilder() : app;
198 this.locale = loc;
199 }
200
201 /**
202 * Constructs a new <code>Formatter</code> using the default
203 * locale and character set, with the specified file as the
204 * output stream.
205 *
206 * @param file the file to use for output.
207 * @throws FileNotFoundException if the file does not exist
208 * and can not be created.
209 * @throws SecurityException if a security manager is present
210 * and doesn't allow writing to the file.
211 */
212 public Formatter(File file)
213 throws FileNotFoundException
214 {
215 this(new OutputStreamWriter(new FileOutputStream(file)));
216 }
217
218 /**
219 * Constructs a new <code>Formatter</code> using the default
220 * locale, with the specified file as the output stream
221 * and the supplied character set.
222 *
223 * @param file the file to use for output.
224 * @param charset the character set to use for output.
225 * @throws FileNotFoundException if the file does not exist
226 * and can not be created.
227 * @throws SecurityException if a security manager is present
228 * and doesn't allow writing to the file.
229 * @throws UnsupportedEncodingException if the supplied character
230 * set is not supported.
231 */
232 public Formatter(File file, String charset)
233 throws FileNotFoundException, UnsupportedEncodingException
234 {
235 this(file, charset, Locale.getDefault());
236 }
237
238 /**
239 * Constructs a new <code>Formatter</code> using the specified
240 * file as the output stream with the supplied character set
241 * and locale. If the locale is <code>null</code>, then no
242 * localization is applied.
243 *
244 * @param file the file to use for output.
245 * @param charset the character set to use for output.
246 * @param loc the locale to use.
247 * @throws FileNotFoundException if the file does not exist
248 * and can not be created.
249 * @throws SecurityException if a security manager is present
250 * and doesn't allow writing to the file.
251 * @throws UnsupportedEncodingException if the supplied character
252 * set is not supported.
253 */
254 public Formatter(File file, String charset, Locale loc)
255 throws FileNotFoundException, UnsupportedEncodingException
256 {
257 this(new OutputStreamWriter(new FileOutputStream(file), charset),
258 loc);
259 }
260
261 /**
262 * Constructs a new <code>Formatter</code> using the default
263 * locale and character set, with the specified output stream.
264 *
265 * @param out the output stream to use.
266 */
267 public Formatter(OutputStream out)
268 {
269 this(new OutputStreamWriter(out));
270 }
271
272 /**
273 * Constructs a new <code>Formatter</code> using the default
274 * locale, with the specified file output stream and the
275 * supplied character set.
276 *
277 * @param out the output stream.
278 * @param charset the character set to use for output.
279 * @throws UnsupportedEncodingException if the supplied character
280 * set is not supported.
281 */
282 public Formatter(OutputStream out, String charset)
283 throws UnsupportedEncodingException
284 {
285 this(out, charset, Locale.getDefault());
286 }
287
288 /**
289 * Constructs a new <code>Formatter</code> using the specified
290 * output stream with the supplied character set and locale.
291 * If the locale is <code>null</code>, then no localization is
292 * applied.
293 *
294 * @param file the output stream.
295 * @param charset the character set to use for output.
296 * @param loc the locale to use.
297 * @throws UnsupportedEncodingException if the supplied character
298 * set is not supported.
299 */
300 public Formatter(OutputStream out, String charset, Locale loc)
301 throws UnsupportedEncodingException
302 {
303 this(new OutputStreamWriter(out, charset), loc);
304 }
305
306 /**
307 * Constructs a new <code>Formatter</code> using the default
308 * locale with the specified output stream. The character
309 * set used is that of the output stream.
310 *
311 * @param out the output stream to use.
312 */
313 public Formatter(PrintStream out)
314 {
315 this((Appendable) out);
316 }
317
318 /**
319 * Constructs a new <code>Formatter</code> using the default
320 * locale and character set, with the specified file as the
321 * output stream.
322 *
323 * @param file the file to use for output.
324 * @throws FileNotFoundException if the file does not exist
325 * and can not be created.
326 * @throws SecurityException if a security manager is present
327 * and doesn't allow writing to the file.
328 */
329 public Formatter(String file) throws FileNotFoundException
330 {
331 this(new OutputStreamWriter(new FileOutputStream(file)));
332 }
333
334 /**
335 * Constructs a new <code>Formatter</code> using the default
336 * locale, with the specified file as the output stream
337 * and the supplied character set.
338 *
339 * @param file the file to use for output.
340 * @param charset the character set to use for output.
341 * @throws FileNotFoundException if the file does not exist
342 * and can not be created.
343 * @throws SecurityException if a security manager is present
344 * and doesn't allow writing to the file.
345 * @throws UnsupportedEncodingException if the supplied character
346 * set is not supported.
347 */
348 public Formatter(String file, String charset)
349 throws FileNotFoundException, UnsupportedEncodingException
350 {
351 this(file, charset, Locale.getDefault());
352 }
353
354 /**
355 * Constructs a new <code>Formatter</code> using the specified
356 * file as the output stream with the supplied character set
357 * and locale. If the locale is <code>null</code>, then no
358 * localization is applied.
359 *
360 * @param file the file to use for output.
361 * @param charset the character set to use for output.
362 * @param loc the locale to use.
363 * @throws FileNotFoundException if the file does not exist
364 * and can not be created.
365 * @throws SecurityException if a security manager is present
366 * and doesn't allow writing to the file.
367 * @throws UnsupportedEncodingException if the supplied character
368 * set is not supported.
369 */
370 public Formatter(String file, String charset, Locale loc)
371 throws FileNotFoundException, UnsupportedEncodingException
372 {
373 this(new OutputStreamWriter(new FileOutputStream(file), charset),
374 loc);
375 }
376
377 /**
378 * Closes the formatter, so as to release used resources.
379 * If the underlying output stream supports the {@link Closeable}
380 * interface, then this is also closed. Attempts to use
381 * a formatter instance, via any method other than
382 * {@link #ioException()}, after closure results in a
383 * {@link FormatterClosedException}.
384 */
385 public void close()
386 {
387 if (closed)
388 return;
389 try
390 {
391 if (out instanceof Closeable)
392 ((Closeable) out).close();
393 }
394 catch (IOException _)
395 {
396 // FIXME: do we ignore these or do we set ioException?
397 // The docs seem to indicate that we should ignore.
398 }
399 closed = true;
400 }
401
402 /**
403 * Flushes the formatter, writing any cached data to the output
404 * stream. If the underlying output stream supports the
405 * {@link Flushable} interface, it is also flushed.
406 *
407 * @throws FormatterClosedException if the formatter is closed.
408 */
409 public void flush()
410 {
411 if (closed)
412 throw new FormatterClosedException();
413 try
414 {
415 if (out instanceof Flushable)
416 ((Flushable) out).flush();
417 }
418 catch (IOException _)
419 {
420 // FIXME: do we ignore these or do we set ioException?
421 // The docs seem to indicate that we should ignore.
422 }
423 }
424
425 /**
426 * Return the name corresponding to a flag.
427 *
428 * @param flags the flag to return the name of.
429 * @return the name of the flag.
430 */
431 private String getName(int flags)
432 {
433 // FIXME: do we want all the flags in here?
434 // Or should we redo how this is reported?
435 int bit = Integer.numberOfTrailingZeros(flags);
436 return FLAGS.substring(bit, bit + 1);
437 }
438
439 /**
440 * Verify the flags passed to a conversion.
441 *
442 * @param flags the flags to verify.
443 * @param allowed the allowed flags mask.
444 * @param conversion the conversion character.
445 */
446 private void checkFlags(int flags, int allowed, char conversion)
447 {
448 flags &= ~allowed;
449 if (flags != 0)
450 throw new FormatFlagsConversionMismatchException(getName(flags),
451 conversion);
452 }
453
454 /**
455 * Throw an exception if a precision was specified.
456 *
457 * @param precision the precision value (-1 indicates not specified).
458 */
459 private void noPrecision(int precision)
460 {
461 if (precision != -1)
462 throw new IllegalFormatPrecisionException(precision);
463 }
464
465 /**
466 * Apply the numeric localization algorithm to a StringBuilder.
467 *
468 * @param builder the builder to apply to.
469 * @param flags the formatting flags to use.
470 * @param width the width of the numeric value.
471 * @param isNegative true if the value is negative.
472 */
473 private void applyLocalization(StringBuilder builder, int flags, int width,
474 boolean isNegative)
475 {
476 DecimalFormatSymbols dfsyms;
477 if (fmtLocale == null)
478 dfsyms = new DecimalFormatSymbols();
479 else
480 dfsyms = new DecimalFormatSymbols(fmtLocale);
481
482 // First replace each digit.
483 char zeroDigit = dfsyms.getZeroDigit();
484 int decimalOffset = -1;
485 for (int i = builder.length() - 1; i >= 0; --i)
486 {
487 char c = builder.charAt(i);
488 if (c >= '0' && c <= '9')
489 builder.setCharAt(i, (char) (c - '0' + zeroDigit));
490 else if (c == '.')
491 {
492 assert decimalOffset == -1;
493 decimalOffset = i;
494 }
495 }
496
497 // Localize the decimal separator.
498 if (decimalOffset != -1)
499 {
500 builder.deleteCharAt(decimalOffset);
501 builder.insert(decimalOffset, dfsyms.getDecimalSeparator());
502 }
503
504 // Insert the grouping separators.
505 if ((flags & FormattableFlags.COMMA) != 0)
506 {
507 char groupSeparator = dfsyms.getGroupingSeparator();
508 int groupSize = 3; // FIXME
509 int offset = (decimalOffset == -1) ? builder.length() : decimalOffset;
510 // We use '>' because we don't want to insert a separator
511 // before the first digit.
512 for (int i = offset - groupSize; i > 0; i -= groupSize)
513 builder.insert(i, groupSeparator);
514 }
515
516 if ((flags & FormattableFlags.ZERO) != 0)
517 {
518 // Zero fill. Note that according to the algorithm we do not
519 // insert grouping separators here.
520 for (int i = width - builder.length(); i > 0; --i)
521 builder.insert(0, zeroDigit);
522 }
523
524 if (isNegative)
525 {
526 if ((flags & FormattableFlags.PAREN) != 0)
527 {
528 builder.insert(0, '(');
529 builder.append(')');
530 }
531 else
532 builder.insert(0, '-');
533 }
534 else if ((flags & FormattableFlags.PLUS) != 0)
535 builder.insert(0, '+');
536 else if ((flags & FormattableFlags.SPACE) != 0)
537 builder.insert(0, ' ');
538 }
539
540 /**
541 * A helper method that handles emitting a String after applying
542 * precision, width, justification, and upper case flags.
543 *
544 * @param arg the string to emit.
545 * @param flags the formatting flags to use.
546 * @param width the width to use.
547 * @param precision the precision to use.
548 * @throws IOException if the output stream throws an I/O error.
549 */
550 private void genericFormat(String arg, int flags, int width, int precision)
551 throws IOException
552 {
553 if ((flags & FormattableFlags.UPPERCASE) != 0)
554 {
555 if (fmtLocale == null)
556 arg = arg.toUpperCase();
557 else
558 arg = arg.toUpperCase(fmtLocale);
559 }
560
561 if (precision >= 0 && arg.length() > precision)
562 arg = arg.substring(0, precision);
563
564 boolean leftJustify = (flags & FormattableFlags.LEFT_JUSTIFY) != 0;
565 if (leftJustify && width == -1)
566 throw new MissingFormatWidthException("fixme");
567 if (! leftJustify && arg.length() < width)
568 {
569 for (int i = width - arg.length(); i > 0; --i)
570 out.append(' ');
571 }
572 out.append(arg);
573 if (leftJustify && arg.length() < width)
574 {
575 for (int i = width - arg.length(); i > 0; --i)
576 out.append(' ');
577 }
578 }
579
580 /**
581 * Emit a boolean.
582 *
583 * @param arg the boolean to emit.
584 * @param flags the formatting flags to use.
585 * @param width the width to use.
586 * @param precision the precision to use.
587 * @param conversion the conversion character.
588 * @throws IOException if the output stream throws an I/O error.
589 */
590 private void booleanFormat(Object arg, int flags, int width, int precision,
591 char conversion)
592 throws IOException
593 {
594 checkFlags(flags,
595 FormattableFlags.LEFT_JUSTIFY | FormattableFlags.UPPERCASE,
596 conversion);
597 String result;
598 if (arg instanceof Boolean)
599 result = String.valueOf((Boolean) arg);
600 else
601 result = arg == null ? "false" : "true";
602 genericFormat(result, flags, width, precision);
603 }
604
605 /**
606 * Emit a hash code.
607 *
608 * @param arg the hash code to emit.
609 * @param flags the formatting flags to use.
610 * @param width the width to use.
611 * @param precision the precision to use.
612 * @param conversion the conversion character.
613 * @throws IOException if the output stream throws an I/O error.
614 */
615 private void hashCodeFormat(Object arg, int flags, int width, int precision,
616 char conversion)
617 throws IOException
618 {
619 checkFlags(flags,
620 FormattableFlags.LEFT_JUSTIFY | FormattableFlags.UPPERCASE,
621 conversion);
622 genericFormat(arg == null ? "null" : Integer.toHexString(arg.hashCode()),
623 flags, width, precision);
624 }
625
626 /**
627 * Emit a String or Formattable conversion.
628 *
629 * @param arg the String or Formattable to emit.
630 * @param flags the formatting flags to use.
631 * @param width the width to use.
632 * @param precision the precision to use.
633 * @param conversion the conversion character.
634 * @throws IOException if the output stream throws an I/O error.
635 */
636 private void stringFormat(Object arg, int flags, int width, int precision,
637 char conversion)
638 throws IOException
639 {
640 if (arg instanceof Formattable)
641 {
642 checkFlags(flags,
643 (FormattableFlags.LEFT_JUSTIFY
644 | FormattableFlags.UPPERCASE
645 | FormattableFlags.ALTERNATE),
646 conversion);
647 Formattable fmt = (Formattable) arg;
648 fmt.formatTo(this, flags, width, precision);
649 }
650 else
651 {
652 checkFlags(flags,
653 FormattableFlags.LEFT_JUSTIFY | FormattableFlags.UPPERCASE,
654 conversion);
655 genericFormat(arg == null ? "null" : arg.toString(), flags, width,
656 precision);
657 }
658 }
659
660 /**
661 * Emit a character.
662 *
663 * @param arg the character to emit.
664 * @param flags the formatting flags to use.
665 * @param width the width to use.
666 * @param precision the precision to use.
667 * @param conversion the conversion character.
668 * @throws IOException if the output stream throws an I/O error.
669 */
670 private void characterFormat(Object arg, int flags, int width, int precision,
671 char conversion)
672 throws IOException
673 {
674 checkFlags(flags,
675 FormattableFlags.LEFT_JUSTIFY | FormattableFlags.UPPERCASE,
676 conversion);
677 noPrecision(precision);
678
679 int theChar;
680 if (arg instanceof Character)
681 theChar = ((Character) arg).charValue();
682 else if (arg instanceof Byte)
683 theChar = (char) (((Byte) arg).byteValue ());
684 else if (arg instanceof Short)
685 theChar = (char) (((Short) arg).shortValue ());
686 else if (arg instanceof Integer)
687 {
688 theChar = ((Integer) arg).intValue();
689 if (! Character.isValidCodePoint(theChar))
690 throw new IllegalFormatCodePointException(theChar);
691 }
692 else
693 throw new IllegalFormatConversionException(conversion, arg.getClass());
694 String result = new String(Character.toChars(theChar));
695 genericFormat(result, flags, width, precision);
696 }
697
698 /**
699 * Emit a '%'.
700 *
701 * @param flags the formatting flags to use.
702 * @param width the width to use.
703 * @param precision the precision to use.
704 * @throws IOException if the output stream throws an I/O error.
705 */
706 private void percentFormat(int flags, int width, int precision)
707 throws IOException
708 {
709 checkFlags(flags, FormattableFlags.LEFT_JUSTIFY, '%');
710 noPrecision(precision);
711 genericFormat("%", flags, width, precision);
712 }
713
714 /**
715 * Emit a newline.
716 *
717 * @param flags the formatting flags to use.
718 * @param width the width to use.
719 * @param precision the precision to use.
720 * @throws IOException if the output stream throws an I/O error.
721 */
722 private void newLineFormat(int flags, int width, int precision)
723 throws IOException
724 {
725 checkFlags(flags, 0, 'n');
726 noPrecision(precision);
727 if (width != -1)
728 throw new IllegalFormatWidthException(width);
729 genericFormat(lineSeparator, flags, width, precision);
730 }
731
732 /**
733 * Helper method to do initial formatting and checking for integral
734 * conversions.
735 *
736 * @param arg the formatted argument.
737 * @param flags the formatting flags to use.
738 * @param width the width to use.
739 * @param precision the precision to use.
740 * @param radix the radix of the number.
741 * @param conversion the conversion character.
742 * @return the result.
743 */
744 private StringBuilder basicIntegralConversion(Object arg, int flags,
745 int width, int precision,
746 int radix, char conversion)
747 {
748 assert radix == 8 || radix == 10 || radix == 16;
749 noPrecision(precision);
750
751 // Some error checking.
752 if ((flags & FormattableFlags.PLUS) != 0
753 && (flags & FormattableFlags.SPACE) != 0)
754 throw new IllegalFormatFlagsException(getName(flags));
755
756 if ((flags & FormattableFlags.LEFT_JUSTIFY) != 0 && width == -1)
757 throw new MissingFormatWidthException("fixme");
758
759 // Do the base translation of the value to a string.
760 String result;
761 int basicFlags = (FormattableFlags.LEFT_JUSTIFY
762 // We already handled any possible error when
763 // parsing.
764 | FormattableFlags.UPPERCASE
765 | FormattableFlags.ZERO);
766 if (radix == 10)
767 basicFlags |= (FormattableFlags.PLUS
768 | FormattableFlags.SPACE
769 | FormattableFlags.COMMA
770 | FormattableFlags.PAREN);
771 else
772 basicFlags |= FormattableFlags.ALTERNATE;
773
774 if (arg instanceof BigInteger)
775 {
776 checkFlags(flags,
777 (basicFlags
778 | FormattableFlags.PLUS
779 | FormattableFlags.SPACE
780 | FormattableFlags.PAREN),
781 conversion);
782 BigInteger bi = (BigInteger) arg;
783 result = bi.toString(radix);
784 }
785 else if (arg instanceof Number
786 && ! (arg instanceof Float)
787 && ! (arg instanceof Double))
788 {
789 checkFlags(flags, basicFlags, conversion);
790 long value = ((Number) arg).longValue ();
791 if (radix == 8)
792 result = Long.toOctalString(value);
793 else if (radix == 16)
794 result = Long.toHexString(value);
795 else
796 result = Long.toString(value);
797 }
798 else
799 throw new IllegalFormatConversionException(conversion, arg.getClass());
800
801 return new StringBuilder(result);
802 }
803
804 /**
805 * Emit a hex or octal value.
806 *
807 * @param arg the hexadecimal or octal value.
808 * @param flags the formatting flags to use.
809 * @param width the width to use.
810 * @param precision the precision to use.
811 * @param radix the radix of the number.
812 * @param conversion the conversion character.
813 * @throws IOException if the output stream throws an I/O error.
814 */
815 private void hexOrOctalConversion(Object arg, int flags, int width,
816 int precision, int radix,
817 char conversion)
818 throws IOException
819 {
820 assert radix == 8 || radix == 16;
821
822 StringBuilder builder = basicIntegralConversion(arg, flags, width,
823 precision, radix,
824 conversion);
825 int insertPoint = 0;
826
827 // Insert the sign.
828 if (builder.charAt(0) == '-')
829 {
830 // Already inserted. Note that we don't insert a sign, since
831 // the only case where it is needed it BigInteger, and it has
832 // already been inserted by toString.
833 ++insertPoint;
834 }
835 else if ((flags & FormattableFlags.PLUS) != 0)
836 {
837 builder.insert(insertPoint, '+');
838 ++insertPoint;
839 }
840 else if ((flags & FormattableFlags.SPACE) != 0)
841 {
842 builder.insert(insertPoint, ' ');
843 ++insertPoint;
844 }
845
846 // Insert the radix prefix.
847 if ((flags & FormattableFlags.ALTERNATE) != 0)
848 {
849 builder.insert(insertPoint, radix == 8 ? "0" : "0x");
850 insertPoint += radix == 8 ? 1 : 2;
851 }
852
853 // Now justify the result.
854 int resultWidth = builder.length();
855 if (resultWidth < width)
856 {
857 char fill = ((flags & FormattableFlags.ZERO) != 0) ? '0' : ' ';
858 if ((flags & FormattableFlags.LEFT_JUSTIFY) != 0)
859 {
860 // Left justify.
861 if (fill == ' ')
862 insertPoint = builder.length();
863 }
864 else
865 {
866 // Right justify. Insert spaces before the radix prefix
867 // and sign.
868 insertPoint = 0;
869 }
870 while (resultWidth++ < width)
871 builder.insert(insertPoint, fill);
872 }
873
874 String result = builder.toString();
875 if ((flags & FormattableFlags.UPPERCASE) != 0)
876 {
877 if (fmtLocale == null)
878 result = result.toUpperCase();
879 else
880 result = result.toUpperCase(fmtLocale);
881 }
882
883 out.append(result);
884 }
885
886 /**
887 * Emit a decimal value.
888 *
889 * @param arg the hexadecimal or octal value.
890 * @param flags the formatting flags to use.
891 * @param width the width to use.
892 * @param precision the precision to use.
893 * @param conversion the conversion character.
894 * @throws IOException if the output stream throws an I/O error.
895 */
896 private void decimalConversion(Object arg, int flags, int width,
897 int precision, char conversion)
898 throws IOException
899 {
900 StringBuilder builder = basicIntegralConversion(arg, flags, width,
901 precision, 10,
902 conversion);
903 boolean isNegative = false;
904 if (builder.charAt(0) == '-')
905 {
906 // Sign handling is done during localization.
907 builder.deleteCharAt(0);
908 isNegative = true;
909 }
910
911 applyLocalization(builder, flags, width, isNegative);
912 genericFormat(builder.toString(), flags, width, precision);
913 }
914
915 /**
916 * Emit a single date or time conversion to a StringBuilder.
917 *
918 * @param builder the builder to write to.
919 * @param cal the calendar to use in the conversion.
920 * @param conversion the formatting character to specify the type of data.
921 * @param syms the date formatting symbols.
922 */
923 private void singleDateTimeConversion(StringBuilder builder, Calendar cal,
924 char conversion,
925 DateFormatSymbols syms)
926 {
927 int oldLen = builder.length();
928 int digits = -1;
929 switch (conversion)
930 {
931 case 'H':
932 builder.append(cal.get(Calendar.HOUR_OF_DAY));
933 digits = 2;
934 break;
935 case 'I':
936 builder.append(cal.get(Calendar.HOUR));
937 digits = 2;
938 break;
939 case 'k':
940 builder.append(cal.get(Calendar.HOUR_OF_DAY));
941 break;
942 case 'l':
943 builder.append(cal.get(Calendar.HOUR));
944 break;
945 case 'M':
946 builder.append(cal.get(Calendar.MINUTE));
947 digits = 2;
948 break;
949 case 'S':
950 builder.append(cal.get(Calendar.SECOND));
951 digits = 2;
952 break;
953 case 'N':
954 // FIXME: nanosecond ...
955 digits = 9;
956 break;
957 case 'p':
958 {
959 int ampm = cal.get(Calendar.AM_PM);
960 builder.append(syms.getAmPmStrings()[ampm]);
961 }
962 break;
963 case 'z':
964 {
965 int zone = cal.get(Calendar.ZONE_OFFSET) / (1000 * 60);
966 builder.append(zone);
967 digits = 4;
968 // Skip the '-' sign.
969 if (zone < 0)
970 ++oldLen;
971 }
972 break;
973 case 'Z':
974 {
975 // FIXME: DST?
976 int zone = cal.get(Calendar.ZONE_OFFSET) / (1000 * 60 * 60);
977 String[][] zs = syms.getZoneStrings();
978 builder.append(zs[zone + 12][1]);
979 }
980 break;
981 case 's':
982 {
983 long val = cal.getTime().getTime();
984 builder.append(val / 1000);
985 }
986 break;
987 case 'Q':
988 {
989 long val = cal.getTime().getTime();
990 builder.append(val);
991 }
992 break;
993 case 'B':
994 {
995 int month = cal.get(Calendar.MONTH);
996 builder.append(syms.getMonths()[month]);
997 }
998 break;
999 case 'b':
1000 case 'h':
1001 {
1002 int month = cal.get(Calendar.MONTH);
1003 builder.append(syms.getShortMonths()[month]);
1004 }
1005 break;
1006 case 'A':
1007 {
1008 int day = cal.get(Calendar.DAY_OF_WEEK);
1009 builder.append(syms.getWeekdays()[day]);
1010 }
1011 break;
1012 case 'a':
1013 {
1014 int day = cal.get(Calendar.DAY_OF_WEEK);
1015 builder.append(syms.getShortWeekdays()[day]);
1016 }
1017 break;
1018 case 'C':
1019 builder.append(cal.get(Calendar.YEAR) / 100);
1020 digits = 2;
1021 break;
1022 case 'Y':
1023 builder.append(cal.get(Calendar.YEAR));
1024 digits = 4;
1025 break;
1026 case 'y':
1027 builder.append(cal.get(Calendar.YEAR) % 100);
1028 digits = 2;
1029 break;
1030 case 'j':
1031 builder.append(cal.get(Calendar.DAY_OF_YEAR));
1032 digits = 3;
1033 break;
1034 case 'm':
1035 builder.append(cal.get(Calendar.MONTH) + 1);
1036 digits = 2;
1037 break;
1038 case 'd':
1039 builder.append(cal.get(Calendar.DAY_OF_MONTH));
1040 digits = 2;
1041 break;
1042 case 'e':
1043 builder.append(cal.get(Calendar.DAY_OF_MONTH));
1044 break;
1045 case 'R':
1046 singleDateTimeConversion(builder, cal, 'H', syms);
1047 builder.append(':');
1048 singleDateTimeConversion(builder, cal, 'M', syms);
1049 break;
1050 case 'T':
1051 singleDateTimeConversion(builder, cal, 'H', syms);
1052 builder.append(':');
1053 singleDateTimeConversion(builder, cal, 'M', syms);
1054 builder.append(':');
1055 singleDateTimeConversion(builder, cal, 'S', syms);
1056 break;
1057 case 'r':
1058 singleDateTimeConversion(builder, cal, 'I', syms);
1059 builder.append(':');
1060 singleDateTimeConversion(builder, cal, 'M', syms);
1061 builder.append(':');
1062 singleDateTimeConversion(builder, cal, 'S', syms);
1063 builder.append(' ');
1064 singleDateTimeConversion(builder, cal, 'p', syms);
1065 break;
1066 case 'D':
1067 singleDateTimeConversion(builder, cal, 'm', syms);
1068 builder.append('/');
1069 singleDateTimeConversion(builder, cal, 'd', syms);
1070 builder.append('/');
1071 singleDateTimeConversion(builder, cal, 'y', syms);
1072 break;
1073 case 'F':
1074 singleDateTimeConversion(builder, cal, 'Y', syms);
1075 builder.append('-');
1076 singleDateTimeConversion(builder, cal, 'm', syms);
1077 builder.append('-');
1078 singleDateTimeConversion(builder, cal, 'd', syms);
1079 break;
1080 case 'c':
1081 singleDateTimeConversion(builder, cal, 'a', syms);
1082 builder.append(' ');
1083 singleDateTimeConversion(builder, cal, 'b', syms);
1084 builder.append(' ');
1085 singleDateTimeConversion(builder, cal, 'd', syms);
1086 builder.append(' ');
1087 singleDateTimeConversion(builder, cal, 'T', syms);
1088 builder.append(' ');
1089 singleDateTimeConversion(builder, cal, 'Z', syms);
1090 builder.append(' ');
1091 singleDateTimeConversion(builder, cal, 'Y', syms);
1092 break;
1093 default:
1094 throw new UnknownFormatConversionException(String.valueOf(conversion));
1095 }
1096
1097 if (digits > 0)
1098 {
1099 int newLen = builder.length();
1100 int delta = newLen - oldLen;
1101 while (delta++ < digits)
1102 builder.insert(oldLen, '0');
1103 }
1104 }
1105
1106 /**
1107 * Emit a date or time value.
1108 *
1109 * @param arg the date or time value.
1110 * @param flags the formatting flags to use.
1111 * @param width the width to use.
1112 * @param precision the precision to use.
1113 * @param conversion the conversion character.
1114 * @param subConversion the sub conversion character.
1115 * @throws IOException if the output stream throws an I/O error.
1116 */
1117 private void dateTimeConversion(Object arg, int flags, int width,
1118 int precision, char conversion,
1119 char subConversion)
1120 throws IOException
1121 {
1122 noPrecision(precision);
1123 checkFlags(flags,
1124 FormattableFlags.LEFT_JUSTIFY | FormattableFlags.UPPERCASE,
1125 conversion);
1126
1127 Calendar cal;
1128 if (arg instanceof Calendar)
1129 cal = (Calendar) arg;
1130 else
1131 {
1132 Date date;
1133 if (arg instanceof Date)
1134 date = (Date) arg;
1135 else if (arg instanceof Long)
1136 date = new Date(((Long) arg).longValue());
1137 else
1138 throw new IllegalFormatConversionException(conversion,
1139 arg.getClass());
1140 if (fmtLocale == null)
1141 cal = Calendar.getInstance();
1142 else
1143 cal = Calendar.getInstance(fmtLocale);
1144 cal.setTime(date);
1145 }
1146
1147 // We could try to be more efficient by computing this lazily.
1148 DateFormatSymbols syms;
1149 if (fmtLocale == null)
1150 syms = new DateFormatSymbols();
1151 else
1152 syms = new DateFormatSymbols(fmtLocale);
1153
1154 StringBuilder result = new StringBuilder();
1155 singleDateTimeConversion(result, cal, subConversion, syms);
1156
1157 genericFormat(result.toString(), flags, width, precision);
1158 }
1159
1160 /**
1161 * Advance the internal parsing index, and throw an exception
1162 * on overrun.
1163 *
1164 * @throws IllegalArgumentException on overrun.
1165 */
1166 private void advance()
1167 {
1168 ++index;
1169 if (index >= length)
1170 {
1171 // FIXME: what exception here?
1172 throw new IllegalArgumentException();
1173 }
1174 }
1175
1176 /**
1177 * Parse an integer appearing in the format string. Will return -1
1178 * if no integer was found.
1179 *
1180 * @return the parsed integer.
1181 */
1182 private int parseInt()
1183 {
1184 int start = index;
1185 while (Character.isDigit(format.charAt(index)))
1186 advance();
1187 if (start == index)
1188 return -1;
1189 return Integer.decode(format.substring(start, index));
1190 }
1191
1192 /**
1193 * Parse the argument index. Returns -1 if there was no index, 0 if
1194 * we should re-use the previous index, and a positive integer to
1195 * indicate an absolute index.
1196 *
1197 * @return the parsed argument index.
1198 */
1199 private int parseArgumentIndex()
1200 {
1201 int result = -1;
1202 int start = index;
1203 if (format.charAt(index) == '<')
1204 {
1205 result = 0;
1206 advance();
1207 }
1208 else if (Character.isDigit(format.charAt(index)))
1209 {
1210 result = parseInt();
1211 if (format.charAt(index) == '$')
1212 advance();
1213 else
1214 {
1215 // Reset.
1216 index = start;
1217 result = -1;
1218 }
1219 }
1220 return result;
1221 }
1222
1223 /**
1224 * Parse a set of flags and return a bit mask of values from
1225 * FormattableFlags. Will throw an exception if a flag is
1226 * duplicated.
1227 *
1228 * @return the parsed flags.
1229 */
1230 private int parseFlags()
1231 {
1232 int value = 0;
1233 int start = index;
1234 while (true)
1235 {
1236 int x = FLAGS.indexOf(format.charAt(index));
1237 if (x == -1)
1238 break;
1239 int newValue = 1 << x;
1240 if ((value & newValue) != 0)
1241 throw new DuplicateFormatFlagsException(format.substring(start,
1242 index + 1));
1243 value |= newValue;
1244 advance();
1245 }
1246 return value;
1247 }
1248
1249 /**
1250 * Parse the width part of a format string. Returns -1 if no width
1251 * was specified.
1252 *
1253 * @return the parsed width.
1254 */
1255 private int parseWidth()
1256 {
1257 return parseInt();
1258 }
1259
1260 /**
1261 * If the current character is '.', parses the precision part of a
1262 * format string. Returns -1 if no precision was specified.
1263 *
1264 * @return the parsed precision.
1265 */
1266 private int parsePrecision()
1267 {
1268 if (format.charAt(index) != '.')
1269 return -1;
1270 advance();
1271 int precision = parseInt();
1272 if (precision == -1)
1273 // FIXME
1274 throw new IllegalArgumentException();
1275 return precision;
1276 }
1277
1278 /**
1279 * Outputs a formatted string based on the supplied specification,
1280 * <code>fmt</code>, and its arguments using the specified locale.
1281 * The locale of the formatter does not change as a result; the
1282 * specified locale is just used for this particular formatting
1283 * operation. If the locale is <code>null</code>, then no
1284 * localization is applied.
1285 *
1286 * @param loc the locale to use for this format.
1287 * @param fmt the format specification.
1288 * @param args the arguments to apply to the specification.
1289 * @throws IllegalFormatException if there is a problem with
1290 * the syntax of the format
1291 * specification or a mismatch
1292 * between it and the arguments.
1293 * @throws FormatterClosedException if the formatter is closed.
1294 */
1295 public Formatter format(Locale loc, String fmt, Object... args)
1296 {
1297 if (closed)
1298 throw new FormatterClosedException();
1299
1300 // Note the arguments are indexed starting at 1.
1301 int implicitArgumentIndex = 1;
1302 int previousArgumentIndex = 0;
1303
1304 try
1305 {
1306 fmtLocale = loc;
1307 format = fmt;
1308 length = format.length();
1309 for (index = 0; index < length; ++index)
1310 {
1311 char c = format.charAt(index);
1312 if (c != '%')
1313 {
1314 out.append(c);
1315 continue;
1316 }
1317
1318 int start = index;
1319 advance();
1320
1321 // We do the needed post-processing of this later, when we
1322 // determine whether an argument is actually needed by
1323 // this conversion.
1324 int argumentIndex = parseArgumentIndex();
1325
1326 int flags = parseFlags();
1327 int width = parseWidth();
1328 int precision = parsePrecision();
1329 char origConversion = format.charAt(index);
1330 char conversion = origConversion;
1331 if (Character.isUpperCase(conversion))
1332 {
1333 flags |= FormattableFlags.UPPERCASE;
1334 conversion = Character.toLowerCase(conversion);
1335 }
1336
1337 Object argument = null;
1338 if (conversion == '%' || conversion == 'n')
1339 {
1340 if (argumentIndex != -1)
1341 {
1342 // FIXME: not sure about this.
1343 throw new UnknownFormatConversionException("FIXME");
1344 }
1345 }
1346 else
1347 {
1348 if (argumentIndex == -1)
1349 argumentIndex = implicitArgumentIndex++;
1350 else if (argumentIndex == 0)
1351 argumentIndex = previousArgumentIndex;
1352 // Argument indices start at 1 but array indices at 0.
1353 --argumentIndex;
1354 if (argumentIndex < 0 || argumentIndex >= args.length)
1355 throw new MissingFormatArgumentException(format.substring(start, index));
1356 argument = args[argumentIndex];
1357 }
1358
1359 switch (conversion)
1360 {
1361 case 'b':
1362 booleanFormat(argument, flags, width, precision,
1363 origConversion);
1364 break;
1365 case 'h':
1366 hashCodeFormat(argument, flags, width, precision,
1367 origConversion);
1368 break;
1369 case 's':
1370 stringFormat(argument, flags, width, precision,
1371 origConversion);
1372 break;
1373 case 'c':
1374 characterFormat(argument, flags, width, precision,
1375 origConversion);
1376 break;
1377 case 'd':
1378 checkFlags(flags & FormattableFlags.UPPERCASE, 0, 'd');
1379 decimalConversion(argument, flags, width, precision,
1380 origConversion);
1381 break;
1382 case 'o':
1383 checkFlags(flags & FormattableFlags.UPPERCASE, 0, 'o');
1384 hexOrOctalConversion(argument, flags, width, precision, 8,
1385 origConversion);
1386 break;
1387 case 'x':
1388 hexOrOctalConversion(argument, flags, width, precision, 16,
1389 origConversion);
1390 case 'e':
1391 // scientificNotationConversion();
1392 break;
1393 case 'f':
1394 // floatingDecimalConversion();
1395 break;
1396 case 'g':
1397 // smartFloatingConversion();
1398 break;
1399 case 'a':
1400 // hexFloatingConversion();
1401 break;
1402 case 't':
1403 advance();
1404 char subConversion = format.charAt(index);
1405 dateTimeConversion(argument, flags, width, precision,
1406 origConversion, subConversion);
1407 break;
1408 case '%':
1409 percentFormat(flags, width, precision);
1410 break;
1411 case 'n':
1412 newLineFormat(flags, width, precision);
1413 break;
1414 default:
1415 throw new UnknownFormatConversionException(String.valueOf(origConversion));
1416 }
1417 }
1418 }
1419 catch (IOException exc)
1420 {
1421 ioException = exc;
1422 }
1423 return this;
1424 }
1425
1426 /**
1427 * Outputs a formatted string based on the supplied specification,
1428 * <code>fmt</code>, and its arguments using the formatter's locale.
1429 *
1430 * @param fmt the format specification.
1431 * @param args the arguments to apply to the specification.
1432 * @throws IllegalFormatException if there is a problem with
1433 * the syntax of the format
1434 * specification or a mismatch
1435 * between it and the arguments.
1436 * @throws FormatterClosedException if the formatter is closed.
1437 */
1438 public Formatter format(String format, Object... args)
1439 {
1440 return format(locale, format, args);
1441 }
1442
1443 /**
1444 * Returns the last I/O exception thrown by the
1445 * <code>append()</code> operation of the underlying
1446 * output stream.
1447 *
1448 * @return the last I/O exception.
1449 */
1450 public IOException ioException()
1451 {
1452 return ioException;
1453 }
1454
1455 /**
1456 * Returns the locale used by this formatter.
1457 *
1458 * @return the formatter's locale.
1459 * @throws FormatterClosedException if the formatter is closed.
1460 */
1461 public Locale locale()
1462 {
1463 if (closed)
1464 throw new FormatterClosedException();
1465 return locale;
1466 }
1467
1468 /**
1469 * Returns the output stream used by this formatter.
1470 *
1471 * @return the formatter's output stream.
1472 * @throws FormatterClosedException if the formatter is closed.
1473 */
1474 public Appendable out()
1475 {
1476 if (closed)
1477 throw new FormatterClosedException();
1478 return out;
1479 }
1480
1481 /**
1482 * Returns the result of applying {@link Object#toString()}
1483 * to the underlying output stream. The results returned
1484 * depend on the particular {@link Appendable} being used.
1485 * For example, a {@link StringBuilder} will return the
1486 * formatted output but an I/O stream will not.
1487 *
1488 * @throws FormatterClosedException if the formatter is closed.
1489 */
1490 public String toString()
1491 {
1492 if (closed)
1493 throw new FormatterClosedException();
1494 return out.toString();
1495 }
1496 }