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