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    }