< prev index next >

src/java.base/share/classes/java/util/Formatter.java

Print this page

        

@@ -34,10 +34,11 @@
 import java.io.Flushable;
 import java.io.OutputStream;
 import java.io.OutputStreamWriter;
 import java.io.PrintStream;
 import java.io.UnsupportedEncodingException;
+import java.lang.invoke.*;
 import java.math.BigDecimal;
 import java.math.BigInteger;
 import java.math.MathContext;
 import java.math.RoundingMode;
 import java.nio.charset.Charset;

@@ -46,28 +47,35 @@
 import java.text.DateFormatSymbols;
 import java.text.DecimalFormat;
 import java.text.DecimalFormatSymbols;
 import java.text.NumberFormat;
 import java.text.spi.NumberFormatProvider;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-import java.util.Objects;
 
 import java.time.DateTimeException;
 import java.time.Instant;
 import java.time.ZoneId;
 import java.time.ZoneOffset;
 import java.time.temporal.ChronoField;
 import java.time.temporal.TemporalAccessor;
 import java.time.temporal.TemporalQueries;
 import java.time.temporal.UnsupportedTemporalTypeException;
 
+import java.lang.compiler.IntrinsicCandidate;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.IntStream;
+
 import jdk.internal.math.DoubleConsts;
 import jdk.internal.math.FormattedFloatingDecimal;
 import sun.util.locale.provider.LocaleProviderAdapter;
 import sun.util.locale.provider.ResourceBundleBasedAdapter;
 
+import static java.lang.invoke.MethodHandles.*;
+import static java.lang.invoke.MethodHandles.constant;
+import static java.lang.invoke.MethodHandles.filterArguments;
+import static java.lang.invoke.MethodType.methodType;
+
 /**
  * An interpreter for printf-style format strings.  This class provides support
  * for layout justification and alignment, common formats for numeric, string,
  * and date/time data, and locale-specific output.  Common Java types such as
  * {@code byte}, {@link java.math.BigDecimal BigDecimal}, and {@link Calendar}

@@ -1911,21 +1919,25 @@
  *
  * @author  Iris Clark
  * @since 1.5
  */
 public final class Formatter implements Closeable, Flushable {
-    private Appendable a;
-    private final Locale l;
-
-    private IOException lastException;
-
-    private final char zero;
-    private static double scaleUp;
-
-    // 1 (sign) + 19 (max # sig digits) + 1 ('.') + 1 ('e') + 1 (sign)
-    // + 3 (max # exp digits) + 4 (error) = 30
-    private static final int MAX_FD_CHARS = 30;
+    /** Receiving Appendable */
+    Appendable a;
+    /** Formatter locale */
+    final Locale l;
+    /** Last low level exception caught */
+    IOException lastException;
+    /** Zero for the locale */
+    final char zero;
+    /** Round up scaler */
+    static double SCALEUP = Math.scalb(1.0, 54);
+    /** Maximum floating decimal digits
+     *    1 (sign) + 19 (max # sig digits) + 1 ('.') + 1 ('e') + 1 (sign)
+     *    + 3 (max # exp digits) + 4 (error) = 30
+     */
+    static final int MAX_FD_CHARS = 30;
 
     /**
      * Returns a charset object for the given charset name.
      * @throws NullPointerException          is csn is null
      * @throws UnsupportedEncodingException  if the charset is not supported

@@ -2603,10 +2615,11 @@
      *          If this formatter has been closed by invoking its {@link
      *          #close()} method
      *
      * @return  This formatter
      */
+    @IntrinsicCandidate
     public Formatter format(String format, Object ... args) {
         return format(l, format, args);
     }
 
     /**

@@ -2642,1606 +2655,1289 @@
      *          If this formatter has been closed by invoking its {@link
      *          #close()} method
      *
      * @return  This formatter
      */
+    @IntrinsicCandidate
     public Formatter format(Locale l, String format, Object ... args) {
+        List<FormatToken> fsa = parse(format);
         ensureOpen();
-
         // index of last argument referenced
         int last = -1;
         // last ordinary index
         int lasto = -1;
 
-        List<FormatString> fsa = parse(format);
-        for (FormatString fs : fsa) {
-            int index = fs.index();
+        for (FormatToken ft : fsa) {
             try {
+                int index = ft.index();
                 switch (index) {
-                case -2:  // fixed string, "%n", or "%%"
-                    fs.print(null, l);
-                    break;
-                case -1:  // relative index
-                    if (last < 0 || (args != null && last > args.length - 1))
-                        throw new MissingFormatArgumentException(fs.toString());
-                    fs.print((args == null ? null : args[last]), l);
-                    break;
-                case 0:  // ordinary index
-                    lasto++;
-                    last = lasto;
-                    if (args != null && lasto > args.length - 1)
-                        throw new MissingFormatArgumentException(fs.toString());
-                    fs.print((args == null ? null : args[lasto]), l);
-                    break;
-                default:  // explicit index
-                    last = index - 1;
-                    if (args != null && last > args.length - 1)
-                        throw new MissingFormatArgumentException(fs.toString());
-                    fs.print((args == null ? null : args[last]), l);
-                    break;
+                    case -2:  // fixed string, "%n", or "%%"
+                        if (ft instanceof FixedString) {
+                            ((FixedString) ft).print(this);
+                        } else {
+                            print((FormatSpecifier) ft, (Object) null, l);
+                        }
+                        break;
+                    case -1:  // relative index
+                        if (last < 0 || (args != null && last > args.length - 1))
+                            throw new MissingFormatArgumentException(ft.toString());
+                        print((FormatSpecifier) ft, (args == null ? null : args[last]), l);
+                        break;
+                    case 0:  // ordinary index
+                        lasto++;
+                        last = lasto;
+                        if (args != null && lasto > args.length - 1)
+                            throw new MissingFormatArgumentException(ft.toString());
+                        print((FormatSpecifier) ft, (args == null ? null : args[lasto]), l);
+                        break;
+                    default:  // explicit index
+                        last = index - 1;
+                        if (args != null && last > args.length - 1)
+                            throw new MissingFormatArgumentException(ft.toString());
+                        print((FormatSpecifier) ft, (args == null ? null : args[last]), l);
+                        break;
                 }
             } catch (IOException x) {
                 lastException = x;
             }
         }
         return this;
     }
 
-    // %[argument_index$][flags][width][.precision][t]conversion
-    private static final String formatSpecifier
-        = "%(\\d+\\$)?([-#+ 0,(\\<]*)?(\\d+)?(\\.\\d+)?([tT])?([a-zA-Z%])";
-
-    private static Pattern fsPattern = Pattern.compile(formatSpecifier);
-
-    /**
-     * Finds format specifiers in the format string.
-     */
-    private List<FormatString> parse(String s) {
-        ArrayList<FormatString> al = new ArrayList<>();
-        Matcher m = fsPattern.matcher(s);
-        for (int i = 0, len = s.length(); i < len; ) {
-            if (m.find(i)) {
-                // Anything between the start of the string and the beginning
-                // of the format specifier is either fixed text or contains
-                // an invalid format string.
-                if (m.start() != i) {
-                    // Make sure we didn't miss any invalid format specifiers
-                    checkText(s, i, m.start());
-                    // Assume previous characters were fixed text
-                    al.add(new FixedString(s, i, m.start()));
-                }
-
-                al.add(new FormatSpecifier(s, m));
-                i = m.end();
-            } else {
-                // No more valid format specifiers.  Check for possible invalid
-                // format specifiers.
-                checkText(s, i, len);
-                // The rest of the string is fixed text
-                al.add(new FixedString(s, i, s.length()));
+    private Formatter print(FormatSpecifier spec, Object arg, Locale l) throws IOException {
+        switch (spec.conversion()) {
+            case DECIMAL_INTEGER:
+            case OCTAL_INTEGER:
+            case HEXADECIMAL_INTEGER:
+                printInteger(spec, arg, l);
                 break;
-            }
-        }
-        return al;
-    }
-
-    private static void checkText(String s, int start, int end) {
-        for (int i = start; i < end; i++) {
-            // Any '%' found in the region starts an invalid format specifier.
-            if (s.charAt(i) == '%') {
-                char c = (i == end - 1) ? '%' : s.charAt(i + 1);
-                throw new UnknownFormatConversionException(String.valueOf(c));
-            }
-        }
-    }
-
-    private interface FormatString {
-        int index();
-        void print(Object arg, Locale l) throws IOException;
-        String toString();
-    }
-
-    private class FixedString implements FormatString {
-        private String s;
-        private int start;
-        private int end;
-        FixedString(String s, int start, int end) {
-            this.s = s;
-            this.start = start;
-            this.end = end;
-        }
-        public int index() { return -2; }
-        public void print(Object arg, Locale l)
-            throws IOException { a.append(s, start, end); }
-        public String toString() { return s.substring(start, end); }
-    }
-
-    /**
-     * Enum for {@code BigDecimal} formatting.
-     */
-    public enum BigDecimalLayoutForm {
-        /**
-         * Format the {@code BigDecimal} in computerized scientific notation.
-         */
-        SCIENTIFIC,
-
-        /**
-         * Format the {@code BigDecimal} as a decimal number.
-         */
-        DECIMAL_FLOAT
-    };
-
-    private class FormatSpecifier implements FormatString {
-        private int index = -1;
-        private Flags f = Flags.NONE;
-        private int width;
-        private int precision;
-        private boolean dt = false;
-        private char c;
-
-        private int index(String s, int start, int end) {
-            if (start >= 0) {
-                try {
-                    // skip the trailing '$'
-                    index = Integer.parseInt(s, start, end - 1, 10);
-                } catch (NumberFormatException x) {
-                    assert(false);
-                }
-            } else {
-                index = 0;
-            }
-            return index;
-        }
-
-        public int index() {
-            return index;
-        }
-
-        private Flags flags(String s, int start, int end) {
-            f = Flags.parse(s, start, end);
-            if (f.contains(Flags.PREVIOUS))
-                index = -1;
-            return f;
-        }
-
-        private int width(String s, int start, int end) {
-            width = -1;
-            if (start >= 0) {
-                try {
-                    width = Integer.parseInt(s, start, end, 10);
-                    if (width < 0)
-                        throw new IllegalFormatWidthException(width);
-                } catch (NumberFormatException x) {
-                    assert(false);
-                }
-            }
-            return width;
-        }
-
-        private int precision(String s, int start, int end) {
-            precision = -1;
-            if (start >= 0) {
-                try {
-                    // skip the leading '.'
-                    precision = Integer.parseInt(s, start + 1, end, 10);
-                    if (precision < 0)
-                        throw new IllegalFormatPrecisionException(precision);
-                } catch (NumberFormatException x) {
-                    assert(false);
-                }
-            }
-            return precision;
-        }
-
-        private char conversion(char conv) {
-            c = conv;
-            if (!dt) {
-                if (!Conversion.isValid(c)) {
-                    throw new UnknownFormatConversionException(String.valueOf(c));
-                }
-                if (Character.isUpperCase(c)) {
-                    f.add(Flags.UPPERCASE);
-                    c = Character.toLowerCase(c);
-                }
-                if (Conversion.isText(c)) {
-                    index = -2;
-                }
-            }
-            return c;
-        }
-
-        FormatSpecifier(String s, Matcher m) {
-            index(s, m.start(1), m.end(1));
-            flags(s, m.start(2), m.end(2));
-            width(s, m.start(3), m.end(3));
-            precision(s, m.start(4), m.end(4));
-
-            int tTStart = m.start(5);
-            if (tTStart >= 0) {
-                dt = true;
-                if (s.charAt(tTStart) == 'T') {
-                    f.add(Flags.UPPERCASE);
-                }
-            }
-            conversion(s.charAt(m.start(6)));
-
-            if (dt)
-                checkDateTime();
-            else if (Conversion.isGeneral(c))
-                checkGeneral();
-            else if (Conversion.isCharacter(c))
-                checkCharacter();
-            else if (Conversion.isInteger(c))
-                checkInteger();
-            else if (Conversion.isFloat(c))
-                checkFloat();
-            else if (Conversion.isText(c))
-                checkText();
-            else
-                throw new UnknownFormatConversionException(String.valueOf(c));
-        }
-
-        public void print(Object arg, Locale l) throws IOException {
-            if (dt) {
-                printDateTime(arg, l);
-                return;
-            }
-            switch(c) {
-            case Conversion.DECIMAL_INTEGER:
-            case Conversion.OCTAL_INTEGER:
-            case Conversion.HEXADECIMAL_INTEGER:
-                printInteger(arg, l);
+            case DATE_TIME:
+                printDateTime(spec, arg, l);
                 break;
-            case Conversion.SCIENTIFIC:
-            case Conversion.GENERAL:
-            case Conversion.DECIMAL_FLOAT:
-            case Conversion.HEXADECIMAL_FLOAT:
-                printFloat(arg, l);
+            case SCIENTIFIC:
+            case GENERAL:
+            case DECIMAL_FLOAT:
+            case HEXADECIMAL_FLOAT:
+                printFloat(spec, arg, l);
                 break;
-            case Conversion.CHARACTER:
-            case Conversion.CHARACTER_UPPER:
-                printCharacter(arg, l);
+            case CHARACTER:
+            case CHARACTER_UPPER:
+                printCharacter(spec, arg, l);
                 break;
-            case Conversion.BOOLEAN:
-                printBoolean(arg, l);
+            case BOOLEAN:
+                printBoolean(spec, arg, l);
                 break;
-            case Conversion.STRING:
-                printString(arg, l);
+            case STRING:
+                printString(spec, arg, l);
                 break;
-            case Conversion.HASHCODE:
-                printHashCode(arg, l);
+            case HASHCODE:
+                printHashCode(spec, arg, l);
                 break;
-            case Conversion.LINE_SEPARATOR:
-                a.append(System.lineSeparator());
+            case LINE_SEPARATOR:
+                out().append(System.lineSeparator());
                 break;
-            case Conversion.PERCENT_SIGN:
-                print("%", l);
+            case PERCENT_SIGN:
+                print(spec, "%", l);
                 break;
             default:
                 assert false;
-            }
         }
+        return this;
+    }
 
-        private void printInteger(Object arg, Locale l) throws IOException {
-            if (arg == null)
-                print("null", l);
-            else if (arg instanceof Byte)
-                print(((Byte)arg).byteValue(), l);
-            else if (arg instanceof Short)
-                print(((Short)arg).shortValue(), l);
-            else if (arg instanceof Integer)
-                print(((Integer)arg).intValue(), l);
-            else if (arg instanceof Long)
-                print(((Long)arg).longValue(), l);
-            else if (arg instanceof BigInteger)
-                print(((BigInteger)arg), l);
-            else
-                failConversion(c, arg);
-        }
+    private void printInteger(FormatSpecifier spec, Object arg, Locale l) throws IOException {
+        if (arg == null)
+            print(spec, "null", l);
+        else if (arg instanceof Byte)
+            print(spec, ((Byte) arg).byteValue(), l);
+        else if (arg instanceof Short)
+            print(spec, ((Short) arg).shortValue(), l);
+        else if (arg instanceof Integer)
+            print(spec, ((Integer) arg).intValue(), l);
+        else if (arg instanceof Long)
+            print(spec, ((Long) arg).longValue(), l);
+        else if (arg instanceof BigInteger)
+            print(spec, (BigInteger) arg, l);
+        else
+            spec.conversion().fail(arg);
+    }
 
-        private void printFloat(Object arg, Locale l) throws IOException {
-            if (arg == null)
-                print("null", l);
-            else if (arg instanceof Float)
-                print(((Float)arg).floatValue(), l);
-            else if (arg instanceof Double)
-                print(((Double)arg).doubleValue(), l);
-            else if (arg instanceof BigDecimal)
-                print(((BigDecimal)arg), l);
-            else
-                failConversion(c, arg);
-        }
+    private void printFloat(FormatSpecifier spec, Object arg, Locale l) throws IOException {
+        if (arg == null)
+            print(spec, "null", l);
+        else if (arg instanceof Float)
+            print(spec, ((Float) arg).floatValue(), l);
+        else if (arg instanceof Double)
+            print(spec, ((Double) arg).doubleValue(), l);
+        else if (arg instanceof BigDecimal)
+            print(spec, (BigDecimal) arg, l);
+        else
+            spec.conversion().fail(arg);
+    }
 
-        private void printDateTime(Object arg, Locale l) throws IOException {
-            if (arg == null) {
-                print("null", l);
-                return;
-            }
-            Calendar cal = null;
-
-            // Instead of Calendar.setLenient(true), perhaps we should
-            // wrap the IllegalArgumentException that might be thrown?
-            if (arg instanceof Long) {
-                // Note that the following method uses an instance of the
-                // default time zone (TimeZone.getDefaultRef().
-                cal = Calendar.getInstance(l == null ? Locale.US : l);
-                cal.setTimeInMillis((Long)arg);
-            } else if (arg instanceof Date) {
-                // Note that the following method uses an instance of the
-                // default time zone (TimeZone.getDefaultRef().
-                cal = Calendar.getInstance(l == null ? Locale.US : l);
-                cal.setTime((Date)arg);
-            } else if (arg instanceof Calendar) {
-                cal = (Calendar) ((Calendar) arg).clone();
-                cal.setLenient(true);
-            } else if (arg instanceof TemporalAccessor) {
-                print((TemporalAccessor) arg, c, l);
-                return;
-            } else {
-                failConversion(c, arg);
-            }
-            // Use the provided locale so that invocations of
-            // localizedMagnitude() use optimizations for null.
-            print(cal, c, l);
-        }
-
-        private void printCharacter(Object arg, Locale l) throws IOException {
-            if (arg == null) {
-                print("null", l);
-                return;
-            }
-            String s = null;
-            if (arg instanceof Character) {
-                s = ((Character)arg).toString();
-            } else if (arg instanceof Byte) {
-                byte i = ((Byte)arg).byteValue();
-                if (Character.isValidCodePoint(i))
-                    s = new String(Character.toChars(i));
-                else
-                    throw new IllegalFormatCodePointException(i);
-            } else if (arg instanceof Short) {
-                short i = ((Short)arg).shortValue();
-                if (Character.isValidCodePoint(i))
-                    s = new String(Character.toChars(i));
-                else
-                    throw new IllegalFormatCodePointException(i);
-            } else if (arg instanceof Integer) {
-                int i = ((Integer)arg).intValue();
-                if (Character.isValidCodePoint(i))
-                    s = new String(Character.toChars(i));
-                else
-                    throw new IllegalFormatCodePointException(i);
-            } else {
-                failConversion(c, arg);
-            }
-            print(s, l);
+    private void printDateTime(FormatSpecifier spec, Object arg, Locale l) throws IOException {
+        if (arg == null) {
+            print(spec, "null", l);
+            return;
         }
+        Calendar cal = null;
 
-        private void printString(Object arg, Locale l) throws IOException {
-            if (arg instanceof Formattable) {
-                Formatter fmt = Formatter.this;
-                if (fmt.locale() != l)
-                    fmt = new Formatter(fmt.out(), l);
-                ((Formattable)arg).formatTo(fmt, f.valueOf(), width, precision);
-            } else {
-                if (f.contains(Flags.ALTERNATE))
-                    failMismatch(Flags.ALTERNATE, 's');
-                if (arg == null)
-                    print("null", l);
-                else
-                    print(arg.toString(), l);
-            }
+        // Instead of Calendar.setLenient(true), perhaps we should
+        // wrap the IllegalArgumentException that might be thrown?
+        if (arg instanceof Long) {
+            // Note that the following method uses an instance of the
+            // default time zone (TimeZone.getDefaultRef().
+            cal = Calendar.getInstance(l == null ? Locale.US : l);
+            cal.setTimeInMillis((Long)arg);
+        } else if (arg instanceof Date) {
+            // Note that the following method uses an instance of the
+            // default time zone (TimeZone.getDefaultRef().
+            cal = Calendar.getInstance(l == null ? Locale.US : l);
+            cal.setTime((Date)arg);
+        } else if (arg instanceof Calendar) {
+            cal = (Calendar) ((Calendar) arg).clone();
+            cal.setLenient(true);
+        } else if (arg instanceof TemporalAccessor) {
+            print(spec, (TemporalAccessor) arg, spec.dateTime(), l);
+            return;
+        } else {
+            spec.conversion().fail(arg);
         }
+        // Use the provided locale so that invocations of
+        // localizedMagnitude() use optimizations for null.
+        print(spec, cal, spec.dateTime(), l);
+    }
 
-        private void printBoolean(Object arg, Locale l) throws IOException {
-            String s;
-            if (arg != null)
-                s = ((arg instanceof Boolean)
-                     ? ((Boolean)arg).toString()
-                     : Boolean.toString(true));
+    private void printCharacter(FormatSpecifier spec, Object arg, Locale l) throws IOException {
+        if (arg == null) {
+            print(spec, "null", l);
+            return;
+        }
+        String s = null;
+        if (arg instanceof Character) {
+            s = ((Character)arg).toString();
+        } else if (arg instanceof Byte) {
+            byte i = (Byte) arg;
+            if (Character.isValidCodePoint(i))
+                s = new String(Character.toChars(i));
             else
-                s = Boolean.toString(false);
-            print(s, l);
+                throw new IllegalFormatCodePointException(i);
+        } else if (arg instanceof Short) {
+            short i = (Short) arg;
+            if (Character.isValidCodePoint(i))
+                s = new String(Character.toChars(i));
+            else
+                throw new IllegalFormatCodePointException(i);
+        } else if (arg instanceof Integer) {
+            int i = (Integer) arg;
+            if (Character.isValidCodePoint(i))
+                s = new String(Character.toChars(i));
+            else
+                throw new IllegalFormatCodePointException(i);
+        } else {
+            spec.conversion().fail(arg);
         }
+        print(spec, s, l);
+    }
 
-        private void printHashCode(Object arg, Locale l) throws IOException {
-            String s = (arg == null
-                        ? "null"
-                        : Integer.toHexString(arg.hashCode()));
-            print(s, l);
+    private void printString(FormatSpecifier spec, Object arg, Locale l) throws IOException {
+        if (arg instanceof Formattable) {
+            Formatter fmt = this;
+            if (fmt.locale() != l)
+                fmt = new Formatter(fmt.out(), l);
+            ((Formattable)arg).formatTo(fmt, spec.flags(), spec.width(), spec.precision());
+        } else {
+            if (Flags.contains(spec.flags(), Flags.ALTERNATE))
+                failMismatch(Flags.ALTERNATE, 's');
+            if (arg == null)
+                print(spec, "null", l);
+            else
+                print(spec, arg.toString(), l);
         }
+    }
 
-        private void print(String s, Locale l) throws IOException {
-            if (precision != -1 && precision < s.length())
-                s = s.substring(0, precision);
-            if (f.contains(Flags.UPPERCASE))
-                s = toUpperCaseWithLocale(s, l);
-            appendJustified(a, s);
-        }
+    private void printBoolean(FormatSpecifier spec, Object arg, Locale l) throws IOException {
+        String s;
+        if (arg != null)
+            s = ((arg instanceof Boolean)
+                    ? ((Boolean)arg).toString()
+                    : Boolean.toString(true));
+        else
+            s = Boolean.toString(false);
+        print(spec, s, l);
+    }
 
-        private String toUpperCaseWithLocale(String s, Locale l) {
-            return s.toUpperCase(Objects.requireNonNullElse(l,
-                    Locale.getDefault(Locale.Category.FORMAT)));
-        }
-
-        private Appendable appendJustified(Appendable a, CharSequence cs) throws IOException {
-             if (width == -1) {
-                 return a.append(cs);
-             }
-             boolean padRight = f.contains(Flags.LEFT_JUSTIFY);
-             int sp = width - cs.length();
-             if (padRight) {
-                 a.append(cs);
-             }
-             for (int i = 0; i < sp; i++) {
-                 a.append(' ');
-             }
-             if (!padRight) {
-                 a.append(cs);
-             }
-             return a;
-        }
+    private Formatter printHashCode(FormatSpecifier spec, Object arg, Locale l) throws IOException {
+        String s = (arg == null
+                ? "null"
+                : Integer.toHexString(arg.hashCode()));
+        print(spec, s, l);
+        return this;
+    }
 
-        public String toString() {
-            StringBuilder sb = new StringBuilder("%");
-            // Flags.UPPERCASE is set internally for legal conversions.
-            Flags dupf = f.dup().remove(Flags.UPPERCASE);
-            sb.append(dupf.toString());
-            if (index > 0)
-                sb.append(index).append('$');
-            if (width != -1)
-                sb.append(width);
-            if (precision != -1)
-                sb.append('.').append(precision);
-            if (dt)
-                sb.append(f.contains(Flags.UPPERCASE) ? 'T' : 't');
-            sb.append(f.contains(Flags.UPPERCASE)
-                      ? Character.toUpperCase(c) : c);
-            return sb.toString();
-        }
+    private Formatter print(FormatSpecifier spec, String s, Locale l) throws IOException {
+        if (spec.precision() != -1 && spec.precision() < s.length())
+            s = s.substring(0, spec.precision());
+        if (Flags.contains(spec.flags(), Flags.UPPERCASE))
+            s = toUpperCaseWithLocale(s, l);
+        appendJustified(spec, a, s);
+        return this;
+    }
 
-        private void checkGeneral() {
-            if ((c == Conversion.BOOLEAN || c == Conversion.HASHCODE)
-                && f.contains(Flags.ALTERNATE))
-                failMismatch(Flags.ALTERNATE, c);
-            // '-' requires a width
-            if (width == -1 && f.contains(Flags.LEFT_JUSTIFY))
-                throw new MissingFormatWidthException(toString());
-            checkBadFlags(Flags.PLUS, Flags.LEADING_SPACE, Flags.ZERO_PAD,
-                          Flags.GROUP, Flags.PARENTHESES);
-        }
+    private String toUpperCaseWithLocale(String s, Locale l) {
+        return s.toUpperCase(Objects.requireNonNullElse(l,
+                Locale.getDefault(Locale.Category.FORMAT)));
+    }
 
-        private void checkDateTime() {
-            if (precision != -1)
-                throw new IllegalFormatPrecisionException(precision);
-            if (!DateTime.isValid(c))
-                throw new UnknownFormatConversionException("t" + c);
-            checkBadFlags(Flags.ALTERNATE, Flags.PLUS, Flags.LEADING_SPACE,
-                          Flags.ZERO_PAD, Flags.GROUP, Flags.PARENTHESES);
-            // '-' requires a width
-            if (width == -1 && f.contains(Flags.LEFT_JUSTIFY))
-                throw new MissingFormatWidthException(toString());
+    private Appendable appendJustified(FormatSpecifier spec, Appendable a, CharSequence cs) throws IOException {
+        if (spec.width() == -1) {
+            return a.append(cs);
+        }
+        boolean padRight = Flags.contains(spec.flags(), Flags.LEFT_JUSTIFY);
+        int sp = spec.width() - cs.length();
+        if (padRight) {
+            a.append(cs);
         }
-
-        private void checkCharacter() {
-            if (precision != -1)
-                throw new IllegalFormatPrecisionException(precision);
-            checkBadFlags(Flags.ALTERNATE, Flags.PLUS, Flags.LEADING_SPACE,
-                          Flags.ZERO_PAD, Flags.GROUP, Flags.PARENTHESES);
-            // '-' requires a width
-            if (width == -1 && f.contains(Flags.LEFT_JUSTIFY))
-                throw new MissingFormatWidthException(toString());
+        for (int i = 0; i < sp; i++) {
+            a.append(' ');
         }
+        if (!padRight) {
+            a.append(cs);
+        }
+        return a;
+    }
 
-        private void checkInteger() {
-            checkNumeric();
-            if (precision != -1)
-                throw new IllegalFormatPrecisionException(precision);
-
-            if (c == Conversion.DECIMAL_INTEGER)
-                checkBadFlags(Flags.ALTERNATE);
-            else if (c == Conversion.OCTAL_INTEGER)
-                checkBadFlags(Flags.GROUP);
-            else
-                checkBadFlags(Flags.GROUP);
+    private Formatter print(FormatSpecifier spec, byte value, Locale l) throws IOException {
+        long v = value;
+        if (value < 0
+                && (spec.conversion() == Conversion.OCTAL_INTEGER
+                || spec.conversion() == Conversion.HEXADECIMAL_INTEGER)) {
+            v += (1L << 8);
+            assert v >= 0 : v;
         }
+        return print(spec, v, l);
+    }
 
-        private void checkBadFlags(Flags ... badFlags) {
-            for (Flags badFlag : badFlags)
-                if (f.contains(badFlag))
-                    failMismatch(badFlag, c);
+    private Formatter print(FormatSpecifier spec, short value, Locale l) throws IOException {
+        long v = value;
+        if (value < 0
+                && (spec.conversion() == Conversion.OCTAL_INTEGER
+                || spec.conversion() == Conversion.HEXADECIMAL_INTEGER)) {
+            v += (1L << 16);
+            assert v >= 0 : v;
         }
+        return print(spec, v, l);
+    }
 
-        private void checkFloat() {
-            checkNumeric();
-            if (c == Conversion.DECIMAL_FLOAT) {
-            } else if (c == Conversion.HEXADECIMAL_FLOAT) {
-                checkBadFlags(Flags.PARENTHESES, Flags.GROUP);
-            } else if (c == Conversion.SCIENTIFIC) {
-                checkBadFlags(Flags.GROUP);
-            } else if (c == Conversion.GENERAL) {
-                checkBadFlags(Flags.ALTERNATE);
-            }
+    private Formatter print(FormatSpecifier spec, int value, Locale l) throws IOException {
+        long v = value;
+        if (value < 0
+                && (spec.conversion() == Conversion.OCTAL_INTEGER
+                || spec.conversion() == Conversion.HEXADECIMAL_INTEGER)) {
+            v += (1L << 32);
+            assert v >= 0 : v;
         }
+        return print(spec, v, l);
+    }
 
-        private void checkNumeric() {
-            if (width != -1 && width < 0)
-                throw new IllegalFormatWidthException(width);
+    private Formatter print(FormatSpecifier spec, long value, Locale l) throws IOException {
+        StringBuilder sb = new StringBuilder();
 
-            if (precision != -1 && precision < 0)
-                throw new IllegalFormatPrecisionException(precision);
+        if (spec.conversion() == Conversion.DECIMAL_INTEGER) {
+            boolean neg = value < 0;
+            String valueStr = Long.toString(value, 10);
 
-            // '-' and '0' require a width
-            if (width == -1
-                && (f.contains(Flags.LEFT_JUSTIFY) || f.contains(Flags.ZERO_PAD)))
-                throw new MissingFormatWidthException(toString());
+            // leading sign indicator
+            leadingSign(spec, sb, neg);
 
-            // bad combination
-            if ((f.contains(Flags.PLUS) && f.contains(Flags.LEADING_SPACE))
-                || (f.contains(Flags.LEFT_JUSTIFY) && f.contains(Flags.ZERO_PAD)))
-                throw new IllegalFormatFlagsException(f.toString());
-        }
+            // the value
+            localizedMagnitude(sb, valueStr, neg ? 1 : 0, spec.flags(), adjustWidth(spec.width(), spec.flags(), neg), l);
 
-        private void checkText() {
-            if (precision != -1)
-                throw new IllegalFormatPrecisionException(precision);
-            switch (c) {
-            case Conversion.PERCENT_SIGN:
-                if (f.valueOf() != Flags.LEFT_JUSTIFY.valueOf()
-                    && f.valueOf() != Flags.NONE.valueOf())
-                    throw new IllegalFormatFlagsException(f.toString());
-                // '-' requires a width
-                if (width == -1 && f.contains(Flags.LEFT_JUSTIFY))
-                    throw new MissingFormatWidthException(toString());
-                break;
-            case Conversion.LINE_SEPARATOR:
-                if (width != -1)
-                    throw new IllegalFormatWidthException(width);
-                if (f.valueOf() != Flags.NONE.valueOf())
-                    throw new IllegalFormatFlagsException(f.toString());
-                break;
-            default:
-                assert false;
-            }
-        }
+            // trailing sign indicator
+            trailingSign(spec, sb, neg);
+        } else if (spec.conversion() == Conversion.OCTAL_INTEGER) {
+            spec.checkBadFlags(Flags.PARENTHESES, Flags.LEADING_SPACE, Flags.PLUS);
+            String s = Long.toOctalString(value);
+            int len = (Flags.contains(spec.flags(), Flags.ALTERNATE)
+                    ? s.length() + 1
+                    : s.length());
 
-        private void print(byte value, Locale l) throws IOException {
-            long v = value;
-            if (value < 0
-                && (c == Conversion.OCTAL_INTEGER
-                    || c == Conversion.HEXADECIMAL_INTEGER)) {
-                v += (1L << 8);
-                assert v >= 0 : v;
+            // apply ALTERNATE (radix indicator for octal) before ZERO_PAD
+            if (Flags.contains(spec.flags(), Flags.ALTERNATE))
+                sb.append('0');
+            if (Flags.contains(spec.flags(), Flags.ZERO_PAD)) {
+                trailingZeros(sb, spec.width() - len);
             }
-            print(v, l);
+            sb.append(s);
+        } else if (spec.conversion() == Conversion.HEXADECIMAL_INTEGER) {
+            spec.checkBadFlags(Flags.PARENTHESES, Flags.LEADING_SPACE,
+                    Flags.PLUS);
+            String s = Long.toHexString(value);
+            int len = (Flags.contains(spec.flags(), Flags.ALTERNATE)
+                    ? s.length() + 2
+                    : s.length());
+
+            // apply ALTERNATE (radix indicator for hex) before ZERO_PAD
+            if (Flags.contains(spec.flags(), Flags.ALTERNATE))
+                sb.append(Flags.contains(spec.flags(), Flags.UPPERCASE) ? "0X" : "0x");
+            if (Flags.contains(spec.flags(), Flags.ZERO_PAD)) {
+                trailingZeros(sb, spec.width() - len);
+            }
+            if (Flags.contains(spec.flags(), Flags.UPPERCASE))
+                s = toUpperCaseWithLocale(s, l);
+            sb.append(s);
         }
 
-        private void print(short value, Locale l) throws IOException {
-            long v = value;
-            if (value < 0
-                && (c == Conversion.OCTAL_INTEGER
-                    || c == Conversion.HEXADECIMAL_INTEGER)) {
-                v += (1L << 16);
-                assert v >= 0 : v;
+        // justify based on width
+        appendJustified(spec, a, sb);
+        return this;
+    }
+
+    // neg := val < 0
+    private void leadingSign(FormatSpecifier spec, StringBuilder sb, boolean neg) {
+        if (!neg) {
+            if (Flags.contains(spec.flags(), Flags.PLUS)) {
+                sb.append('+');
+            } else if (Flags.contains(spec.flags(), Flags.LEADING_SPACE)) {
+                sb.append(' ');
             }
-            print(v, l);
+        } else {
+            if (Flags.contains(spec.flags(), Flags.PARENTHESES))
+                sb.append('(');
+            else
+                sb.append('-');
         }
+    }
+
+    // neg := val < 0
+    private void trailingSign(FormatSpecifier spec, StringBuilder sb, boolean neg) {
+        if (neg && Flags.contains(spec.flags(), Flags.PARENTHESES))
+            sb.append(')');
+    }
 
-        private void print(int value, Locale l) throws IOException {
-            long v = value;
-            if (value < 0
-                && (c == Conversion.OCTAL_INTEGER
-                    || c == Conversion.HEXADECIMAL_INTEGER)) {
-                v += (1L << 32);
-                assert v >= 0 : v;
+    private void print(FormatSpecifier spec, BigInteger value, Locale l) throws IOException {
+        StringBuilder sb = new StringBuilder();
+        boolean neg = value.signum() == -1;
+        BigInteger v = value.abs();
+
+        // leading sign indicator
+        leadingSign(spec, sb, neg);
+
+        // the value
+        if (spec.conversion() == Conversion.DECIMAL_INTEGER) {
+            localizedMagnitude(sb, v.toString(), 0, spec.flags(), adjustWidth(spec.width(), spec.flags(), neg), l);
+        } else if (spec.conversion() == Conversion.OCTAL_INTEGER) {
+            String s = v.toString(8);
+
+            int len = s.length() + sb.length();
+            if (neg && Flags.contains(spec.flags(), Flags.PARENTHESES))
+                len++;
+
+            // apply ALTERNATE (radix indicator for octal) before ZERO_PAD
+            if (Flags.contains(spec.flags(), Flags.ALTERNATE)) {
+                len++;
+                sb.append('0');
+            }
+            if (Flags.contains(spec.flags(), Flags.ZERO_PAD)) {
+                trailingZeros(sb, spec.width() - len);
+            }
+            sb.append(s);
+        } else if (spec.conversion() == Conversion.HEXADECIMAL_INTEGER) {
+            String s = v.toString(16);
+
+            int len = s.length() + sb.length();
+            if (neg && Flags.contains(spec.flags(), Flags.PARENTHESES))
+                len++;
+
+            // apply ALTERNATE (radix indicator for hex) before ZERO_PAD
+            if (Flags.contains(spec.flags(), Flags.ALTERNATE)) {
+                len += 2;
+                sb.append(Flags.contains(spec.flags(), Flags.UPPERCASE) ? "0X" : "0x");
             }
-            print(v, l);
+            if (Flags.contains(spec.flags(), Flags.ZERO_PAD)) {
+                trailingZeros(sb, spec.width() - len);
+            }
+            if (Flags.contains(spec.flags(), Flags.UPPERCASE))
+                s = toUpperCaseWithLocale(s, l);
+            sb.append(s);
         }
 
-        private void print(long value, Locale l) throws IOException {
+        // trailing sign indicator
+        trailingSign(spec, sb, (value.signum() == -1));
 
-            StringBuilder sb = new StringBuilder();
+        // justify based on width
+        appendJustified(spec, a, sb);
+    }
 
-            if (c == Conversion.DECIMAL_INTEGER) {
-                boolean neg = value < 0;
-                String valueStr = Long.toString(value, 10);
-
-                // leading sign indicator
-                leadingSign(sb, neg);
-
-                // the value
-                localizedMagnitude(sb, valueStr, neg ? 1 : 0, f, adjustWidth(width, f, neg), l);
-
-                // trailing sign indicator
-                trailingSign(sb, neg);
-            } else if (c == Conversion.OCTAL_INTEGER) {
-                checkBadFlags(Flags.PARENTHESES, Flags.LEADING_SPACE,
-                              Flags.PLUS);
-                String s = Long.toOctalString(value);
-                int len = (f.contains(Flags.ALTERNATE)
-                           ? s.length() + 1
-                           : s.length());
-
-                // apply ALTERNATE (radix indicator for octal) before ZERO_PAD
-                if (f.contains(Flags.ALTERNATE))
-                    sb.append('0');
-                if (f.contains(Flags.ZERO_PAD)) {
-                    trailingZeros(sb, width - len);
-                }
-                sb.append(s);
-            } else if (c == Conversion.HEXADECIMAL_INTEGER) {
-                checkBadFlags(Flags.PARENTHESES, Flags.LEADING_SPACE,
-                              Flags.PLUS);
-                String s = Long.toHexString(value);
-                int len = (f.contains(Flags.ALTERNATE)
-                           ? s.length() + 2
-                           : s.length());
-
-                // apply ALTERNATE (radix indicator for hex) before ZERO_PAD
-                if (f.contains(Flags.ALTERNATE))
-                    sb.append(f.contains(Flags.UPPERCASE) ? "0X" : "0x");
-                if (f.contains(Flags.ZERO_PAD)) {
-                    trailingZeros(sb, width - len);
-                }
-                if (f.contains(Flags.UPPERCASE))
-                    s = toUpperCaseWithLocale(s, l);
-                sb.append(s);
-            }
-
-            // justify based on width
-            appendJustified(a, sb);
-        }
-
-        // neg := val < 0
-        private StringBuilder leadingSign(StringBuilder sb, boolean neg) {
-            if (!neg) {
-                if (f.contains(Flags.PLUS)) {
-                    sb.append('+');
-                } else if (f.contains(Flags.LEADING_SPACE)) {
-                    sb.append(' ');
-                }
-            } else {
-                if (f.contains(Flags.PARENTHESES))
-                    sb.append('(');
-                else
-                    sb.append('-');
-            }
-            return sb;
-        }
+    private Formatter print(FormatSpecifier spec, float value, Locale l) throws IOException {
+        return print(spec, (double) value, l);
+    }
 
-        // neg := val < 0
-        private StringBuilder trailingSign(StringBuilder sb, boolean neg) {
-            if (neg && f.contains(Flags.PARENTHESES))
-                sb.append(')');
-            return sb;
-        }
+    private Formatter print(FormatSpecifier spec, double value, Locale l) throws IOException {
+        StringBuilder sb = new StringBuilder();
+        boolean neg = Double.compare(value, 0.0) == -1;
 
-        private void print(BigInteger value, Locale l) throws IOException {
-            StringBuilder sb = new StringBuilder();
-            boolean neg = value.signum() == -1;
-            BigInteger v = value.abs();
+        if (!Double.isNaN(value)) {
+            double v = Math.abs(value);
 
             // leading sign indicator
-            leadingSign(sb, neg);
+            leadingSign(spec, sb, neg);
 
             // the value
-            if (c == Conversion.DECIMAL_INTEGER) {
-                localizedMagnitude(sb, v.toString(), 0, f, adjustWidth(width, f, neg), l);
-            } else if (c == Conversion.OCTAL_INTEGER) {
-                String s = v.toString(8);
-
-                int len = s.length() + sb.length();
-                if (neg && f.contains(Flags.PARENTHESES))
-                    len++;
-
-                // apply ALTERNATE (radix indicator for octal) before ZERO_PAD
-                if (f.contains(Flags.ALTERNATE)) {
-                    len++;
-                    sb.append('0');
-                }
-                if (f.contains(Flags.ZERO_PAD)) {
-                    trailingZeros(sb, width - len);
-                }
-                sb.append(s);
-            } else if (c == Conversion.HEXADECIMAL_INTEGER) {
-                String s = v.toString(16);
-
-                int len = s.length() + sb.length();
-                if (neg && f.contains(Flags.PARENTHESES))
-                    len++;
-
-                // apply ALTERNATE (radix indicator for hex) before ZERO_PAD
-                if (f.contains(Flags.ALTERNATE)) {
-                    len += 2;
-                    sb.append(f.contains(Flags.UPPERCASE) ? "0X" : "0x");
-                }
-                if (f.contains(Flags.ZERO_PAD)) {
-                    trailingZeros(sb, width - len);
-                }
-                if (f.contains(Flags.UPPERCASE))
-                    s = toUpperCaseWithLocale(s, l);
-                sb.append(s);
-            }
+            if (!Double.isInfinite(v))
+                print(spec, sb, v, l, spec.flags(), spec.conversion(), spec.precision(), neg);
+            else
+                sb.append(Flags.contains(spec.flags(), Flags.UPPERCASE)
+                        ? "INFINITY" : "Infinity");
 
             // trailing sign indicator
-            trailingSign(sb, (value.signum() == -1));
-
-            // justify based on width
-            appendJustified(a, sb);
-        }
-
-        private void print(float value, Locale l) throws IOException {
-            print((double) value, l);
+            trailingSign(spec, sb, neg);
+        } else {
+            sb.append(Flags.contains(spec.flags(), Flags.UPPERCASE) ? "NAN" : "NaN");
         }
 
-        private void print(double value, Locale l) throws IOException {
-            StringBuilder sb = new StringBuilder();
-            boolean neg = Double.compare(value, 0.0) == -1;
-
-            if (!Double.isNaN(value)) {
-                double v = Math.abs(value);
-
-                // leading sign indicator
-                leadingSign(sb, neg);
-
-                // the value
-                if (!Double.isInfinite(v))
-                    print(sb, v, l, f, c, precision, neg);
-                else
-                    sb.append(f.contains(Flags.UPPERCASE)
-                              ? "INFINITY" : "Infinity");
-
-                // trailing sign indicator
-                trailingSign(sb, neg);
-            } else {
-                sb.append(f.contains(Flags.UPPERCASE) ? "NAN" : "NaN");
-            }
-
-            // justify based on width
-            appendJustified(a, sb);
-        }
+        // justify based on width
+        appendJustified(spec, a, sb);
+        return this;
+    }
 
-        // !Double.isInfinite(value) && !Double.isNaN(value)
-        private void print(StringBuilder sb, double value, Locale l,
-                           Flags f, char c, int precision, boolean neg)
+    // !Double.isInfinite(value) && !Double.isNaN(value)
+    private void print(FormatSpecifier spec, StringBuilder sb, double value, Locale l,
+                       int f, Conversion conversion, int precision, boolean neg)
             throws IOException
-        {
-            if (c == Conversion.SCIENTIFIC) {
-                // Create a new FormattedFloatingDecimal with the desired
-                // precision.
-                int prec = (precision == -1 ? 6 : precision);
+    {
+        if (conversion == Conversion.SCIENTIFIC) {
+            // Create a new FormattedFloatingDecimal with the desired
+            // precision.
+            int prec = (precision == -1 ? 6 : precision);
 
-                FormattedFloatingDecimal fd
-                        = FormattedFloatingDecimal.valueOf(value, prec,
-                          FormattedFloatingDecimal.Form.SCIENTIFIC);
+            FormattedFloatingDecimal fd
+                    = FormattedFloatingDecimal.valueOf(value, prec,
+                    FormattedFloatingDecimal.Form.SCIENTIFIC);
 
-                StringBuilder mant = new StringBuilder().append(fd.getMantissa());
-                addZeros(mant, prec);
+            StringBuilder mant = new StringBuilder().append(fd.getMantissa());
+            addZeros(mant, prec);
 
-                // If the precision is zero and the '#' flag is set, add the
-                // requested decimal point.
-                if (f.contains(Flags.ALTERNATE) && (prec == 0)) {
-                    mant.append('.');
-                }
+            // If the precision is zero and the '#' flag is set, add the
+            // requested decimal point.
+            if (Flags.contains(f, Flags.ALTERNATE) && (prec == 0)) {
+                mant.append('.');
+            }
 
-                char[] exp = (value == 0.0)
+            char[] exp = (value == 0.0)
                     ? new char[] {'+','0','0'} : fd.getExponent();
 
-                int newW = width;
-                if (width != -1) {
-                    newW = adjustWidth(width - exp.length - 1, f, neg);
-                }
-                localizedMagnitude(sb, mant, 0, f, newW, l);
+            int width = spec.width();
+            if (width != -1) {
+                width = adjustWidth(width - exp.length - 1, f, neg);
+            }
+            localizedMagnitude(sb, mant, 0, f, width, l);
 
-                sb.append(f.contains(Flags.UPPERCASE) ? 'E' : 'e');
+            sb.append(Flags.contains(f, Flags.UPPERCASE) ? 'E' : 'e');
 
-                char sign = exp[0];
-                assert(sign == '+' || sign == '-');
-                sb.append(sign);
+            char sign = exp[0];
+            assert(sign == '+' || sign == '-');
+            sb.append(sign);
 
-                localizedMagnitudeExp(sb, exp, 1, l);
-            } else if (c == Conversion.DECIMAL_FLOAT) {
-                // Create a new FormattedFloatingDecimal with the desired
-                // precision.
-                int prec = (precision == -1 ? 6 : precision);
+            localizedMagnitudeExp(spec, sb, exp, 1, l);
+        } else if (conversion == Conversion.DECIMAL_FLOAT) {
+            // Create a new FormattedFloatingDecimal with the desired
+            // precision.
+            int prec = (precision == -1 ? 6 : precision);
 
-                FormattedFloatingDecimal fd
-                        = FormattedFloatingDecimal.valueOf(value, prec,
-                          FormattedFloatingDecimal.Form.DECIMAL_FLOAT);
+            FormattedFloatingDecimal fd
+                    = FormattedFloatingDecimal.valueOf(value, prec,
+                    FormattedFloatingDecimal.Form.DECIMAL_FLOAT);
 
-                StringBuilder mant = new StringBuilder().append(fd.getMantissa());
-                addZeros(mant, prec);
+            StringBuilder mant = new StringBuilder().append(fd.getMantissa());
+            addZeros(mant, prec);
 
-                // If the precision is zero and the '#' flag is set, add the
-                // requested decimal point.
-                if (f.contains(Flags.ALTERNATE) && (prec == 0))
-                    mant.append('.');
+            // If the precision is zero and the '#' flag is set, add the
+            // requested decimal point.
+            if (Flags.contains(f, Flags.ALTERNATE) && (prec == 0))
+                mant.append('.');
 
-                int newW = width;
-                if (width != -1)
-                    newW = adjustWidth(width, f, neg);
-                localizedMagnitude(sb, mant, 0, f, newW, l);
-            } else if (c == Conversion.GENERAL) {
-                int prec = precision;
-                if (precision == -1)
-                    prec = 6;
-                else if (precision == 0)
-                    prec = 1;
-
-                char[] exp;
-                StringBuilder mant = new StringBuilder();
-                int expRounded;
-                if (value == 0.0) {
-                    exp = null;
-                    mant.append('0');
-                    expRounded = 0;
-                } else {
-                    FormattedFloatingDecimal fd
+            int width = spec.width();
+            if (width != -1)
+                width = adjustWidth(width, f, neg);
+            localizedMagnitude(sb, mant, 0, f, width, l);
+        } else if (conversion == Conversion.GENERAL) {
+            int prec = precision;
+            if (precision == -1)
+                prec = 6;
+            else if (precision == 0)
+                prec = 1;
+
+            char[] exp;
+            StringBuilder mant = new StringBuilder();
+            int expRounded;
+            if (value == 0.0) {
+                exp = null;
+                mant.append('0');
+                expRounded = 0;
+            } else {
+                FormattedFloatingDecimal fd
                         = FormattedFloatingDecimal.valueOf(value, prec,
-                          FormattedFloatingDecimal.Form.GENERAL);
-                    exp = fd.getExponent();
-                    mant.append(fd.getMantissa());
-                    expRounded = fd.getExponentRounded();
-                }
-
-                if (exp != null) {
-                    prec -= 1;
-                } else {
-                    prec -= expRounded + 1;
-                }
-
-                addZeros(mant, prec);
-                // If the precision is zero and the '#' flag is set, add the
-                // requested decimal point.
-                if (f.contains(Flags.ALTERNATE) && (prec == 0)) {
-                    mant.append('.');
-                }
-
-                int newW = width;
-                if (width != -1) {
-                    if (exp != null)
-                        newW = adjustWidth(width - exp.length - 1, f, neg);
-                    else
-                        newW = adjustWidth(width, f, neg);
-                }
-                localizedMagnitude(sb, mant, 0, f, newW, l);
-
-                if (exp != null) {
-                    sb.append(f.contains(Flags.UPPERCASE) ? 'E' : 'e');
+                        FormattedFloatingDecimal.Form.GENERAL);
+                exp = fd.getExponent();
+                mant.append(fd.getMantissa());
+                expRounded = fd.getExponentRounded();
+            }
 
-                    char sign = exp[0];
-                    assert(sign == '+' || sign == '-');
-                    sb.append(sign);
+            if (exp != null) {
+                prec -= 1;
+            } else {
+                prec -= expRounded + 1;
+            }
 
-                    localizedMagnitudeExp(sb, exp, 1, l);
-                }
-            } else if (c == Conversion.HEXADECIMAL_FLOAT) {
-                int prec = precision;
-                if (precision == -1)
-                    // assume that we want all of the digits
-                    prec = 0;
-                else if (precision == 0)
-                    prec = 1;
+            addZeros(mant, prec);
+            // If the precision is zero and the '#' flag is set, add the
+            // requested decimal point.
+            if (Flags.contains(f, Flags.ALTERNATE) && (prec == 0)) {
+                mant.append('.');
+            }
 
-                String s = hexDouble(value, prec);
+            int width = spec.width();
+            if (width != -1) {
+                if (exp != null)
+                    width = adjustWidth(width - exp.length - 1, f, neg);
+                else
+                    width = adjustWidth(width, f, neg);
+            }
+            localizedMagnitude(sb, mant, 0, f, width, l);
 
-                StringBuilder va = new StringBuilder();
-                boolean upper = f.contains(Flags.UPPERCASE);
-                sb.append(upper ? "0X" : "0x");
+            if (exp != null) {
+                sb.append(Flags.contains(f, Flags.UPPERCASE) ? 'E' : 'e');
 
-                if (f.contains(Flags.ZERO_PAD)) {
-                    trailingZeros(sb, width - s.length() - 2);
-                }
+                char sign = exp[0];
+                assert(sign == '+' || sign == '-');
+                sb.append(sign);
 
-                int idx = s.indexOf('p');
-                if (upper) {
-                    String tmp = s.substring(0, idx);
-                    // don't localize hex
-                    tmp = tmp.toUpperCase(Locale.ROOT);
-                    va.append(tmp);
-                } else {
-                    va.append(s, 0, idx);
-                }
-                if (prec != 0) {
-                    addZeros(va, prec);
-                }
-                sb.append(va);
-                sb.append(upper ? 'P' : 'p');
-                sb.append(s, idx+1, s.length());
+                localizedMagnitudeExp(spec, sb, exp, 1, l);
             }
-        }
-
-        // Add zeros to the requested precision.
-        private void addZeros(StringBuilder sb, int prec) {
-            // Look for the dot.  If we don't find one, the we'll need to add
-            // it before we add the zeros.
-            int len = sb.length();
-            int i;
-            for (i = 0; i < len; i++) {
-                if (sb.charAt(i) == '.') {
-                    break;
-                }
+        } else if (conversion == Conversion.HEXADECIMAL_FLOAT) {
+            int prec = precision;
+            if (precision == -1)
+                // assume that we want all of the digits
+                prec = 0;
+            else if (precision == 0)
+                prec = 1;
+
+            String s = hexDouble(value, prec);
+
+            StringBuilder va = new StringBuilder();
+            boolean upper = Flags.contains(f, Flags.UPPERCASE);
+            sb.append(upper ? "0X" : "0x");
+
+            if (Flags.contains(f, Flags.ZERO_PAD)) {
+                trailingZeros(sb, spec.width() - s.length() - 2);
+            }
+
+            int idx = s.indexOf('p');
+            if (upper) {
+                String tmp = s.substring(0, idx);
+                // don't localize hex
+                tmp = tmp.toUpperCase(Locale.ROOT);
+                va.append(tmp);
+            } else {
+                va.append(s, 0, idx);
             }
-            boolean needDot = false;
-            if (i == len) {
-                needDot = true;
+            if (prec != 0) {
+                addZeros(va, prec);
             }
+            sb.append(va);
+            sb.append(upper ? 'P' : 'p');
+            sb.append(s, idx+1, s.length());
+        }
+    }
 
-            // Determine existing precision.
-            int outPrec = len - i - (needDot ? 0 : 1);
-            assert (outPrec <= prec);
-            if (outPrec == prec) {
-                return;
+    // Add zeros to the requested precision.
+    private void addZeros(StringBuilder sb, int prec) {
+        // Look for the dot.  If we don't find one, the we'll need to add
+        // it before we add the zeros.
+        int len = sb.length();
+        int i;
+        for (i = 0; i < len; i++) {
+            if (sb.charAt(i) == '.') {
+                break;
             }
+        }
+        boolean needDot = i == len;
 
-            // Add dot if previously determined to be necessary.
-            if (needDot) {
-                sb.append('.');
-            }
+        // Determine existing precision.
+        int outPrec = len - i - (needDot ? 0 : 1);
+        assert (outPrec <= prec);
+        if (outPrec == prec) {
+            return;
+        }
 
-            // Add zeros.
-            trailingZeros(sb, prec - outPrec);
+        // Add dot if previously determined to be necessary.
+        if (needDot) {
+            sb.append('.');
         }
 
-        // Method assumes that d > 0.
-        private String hexDouble(double d, int prec) {
-            // Let Double.toHexString handle simple cases
-            if (!Double.isFinite(d) || d == 0.0 || prec == 0 || prec >= 13) {
-                // remove "0x"
-                return Double.toHexString(d).substring(2);
-            } else {
-                assert(prec >= 1 && prec <= 12);
+        // Add zeros.
+        trailingZeros(sb, prec - outPrec);
+    }
+
+    // Method assumes that d > 0.
+    private String hexDouble(double d, int prec) {
+        // Let Double.toHexString handle simple cases
+        if (!Double.isFinite(d) || d == 0.0 || prec == 0 || prec >= 13) {
+            // remove "0x"
+            return Double.toHexString(d).substring(2);
+        } else {
+            assert(prec >= 1 && prec <= 12);
 
-                int exponent  = Math.getExponent(d);
-                boolean subnormal
+            int exponent  = Math.getExponent(d);
+            boolean subnormal
                     = (exponent == Double.MIN_EXPONENT - 1);
 
-                // If this is subnormal input so normalize (could be faster to
-                // do as integer operation).
-                if (subnormal) {
-                    scaleUp = Math.scalb(1.0, 54);
-                    d *= scaleUp;
-                    // Calculate the exponent.  This is not just exponent + 54
-                    // since the former is not the normalized exponent.
-                    exponent = Math.getExponent(d);
-                    assert exponent >= Double.MIN_EXPONENT &&
+            // If this is subnormal input so normalize (could be faster to
+            // do as integer operation).
+            if (subnormal) {
+                d *= SCALEUP;
+                // Calculate the exponent.  This is not just exponent + 54
+                // since the former is not the normalized exponent.
+                exponent = Math.getExponent(d);
+                assert exponent >= Double.MIN_EXPONENT &&
                         exponent <= Double.MAX_EXPONENT: exponent;
-                }
+            }
 
-                int precision = 1 + prec*4;
-                int shiftDistance
+            int precision = 1 + prec*4;
+            int shiftDistance
                     =  DoubleConsts.SIGNIFICAND_WIDTH - precision;
-                assert(shiftDistance >= 1 && shiftDistance < DoubleConsts.SIGNIFICAND_WIDTH);
+            assert(shiftDistance >= 1 && shiftDistance < DoubleConsts.SIGNIFICAND_WIDTH);
 
-                long doppel = Double.doubleToLongBits(d);
-                // Deterime the number of bits to keep.
-                long newSignif
+            long doppel = Double.doubleToLongBits(d);
+            // Deterime the number of bits to keep.
+            long newSignif
                     = (doppel & (DoubleConsts.EXP_BIT_MASK
-                                 | DoubleConsts.SIGNIF_BIT_MASK))
-                                     >> shiftDistance;
-                // Bits to round away.
-                long roundingBits = doppel & ~(~0L << shiftDistance);
-
-                // To decide how to round, look at the low-order bit of the
-                // working significand, the highest order discarded bit (the
-                // round bit) and whether any of the lower order discarded bits
-                // are nonzero (the sticky bit).
+                    | DoubleConsts.SIGNIF_BIT_MASK))
+                    >> shiftDistance;
+            // Bits to round away.
+            long roundingBits = doppel & ~(~0L << shiftDistance);
+
+            // To decide how to round, look at the low-order bit of the
+            // working significand, the highest order discarded bit (the
+            // round bit) and whether any of the lower order discarded bits
+            // are nonzero (the sticky bit).
 
-                boolean leastZero = (newSignif & 0x1L) == 0L;
-                boolean round
+            boolean leastZero = (newSignif & 0x1L) == 0L;
+            boolean round
                     = ((1L << (shiftDistance - 1) ) & roundingBits) != 0L;
-                boolean sticky  = shiftDistance > 1 &&
+            boolean sticky  = shiftDistance > 1 &&
                     (~(1L<< (shiftDistance - 1)) & roundingBits) != 0;
-                if((leastZero && round && sticky) || (!leastZero && round)) {
-                    newSignif++;
-                }
+            if((leastZero && round && sticky) || (!leastZero && round)) {
+                newSignif++;
+            }
 
-                long signBit = doppel & DoubleConsts.SIGN_BIT_MASK;
-                newSignif = signBit | (newSignif << shiftDistance);
-                double result = Double.longBitsToDouble(newSignif);
+            long signBit = doppel & DoubleConsts.SIGN_BIT_MASK;
+            newSignif = signBit | (newSignif << shiftDistance);
+            double result = Double.longBitsToDouble(newSignif);
 
-                if (Double.isInfinite(result) ) {
-                    // Infinite result generated by rounding
-                    return "1.0p1024";
-                } else {
-                    String res = Double.toHexString(result).substring(2);
-                    if (!subnormal)
-                        return res;
-                    else {
-                        // Create a normalized subnormal string.
-                        int idx = res.indexOf('p');
-                        if (idx == -1) {
-                            // No 'p' character in hex string.
-                            assert false;
-                            return null;
-                        } else {
-                            // Get exponent and append at the end.
-                            String exp = res.substring(idx + 1);
-                            int iexp = Integer.parseInt(exp) -54;
-                            return res.substring(0, idx) + "p"
+            if (Double.isInfinite(result) ) {
+                // Infinite result generated by rounding
+                return "1.0p1024";
+            } else {
+                String res = Double.toHexString(result).substring(2);
+                if (!subnormal)
+                    return res;
+                else {
+                    // Create a normalized subnormal string.
+                    int idx = res.indexOf('p');
+                    if (idx == -1) {
+                        // No 'p' character in hex string.
+                        assert false;
+                        return null;
+                    } else {
+                        // Get exponent and append at the end.
+                        String exp = res.substring(idx + 1);
+                        int iexp = Integer.parseInt(exp) -54;
+                        return res.substring(0, idx) + "p"
                                 + Integer.toString(iexp);
-                        }
                     }
                 }
             }
         }
+    }
 
-        private void print(BigDecimal value, Locale l) throws IOException {
-            if (c == Conversion.HEXADECIMAL_FLOAT)
-                failConversion(c, value);
-            StringBuilder sb = new StringBuilder();
-            boolean neg = value.signum() == -1;
-            BigDecimal v = value.abs();
-            // leading sign indicator
-            leadingSign(sb, neg);
+    private void print(FormatSpecifier spec, BigDecimal value, Locale l) throws IOException {
+        if (spec.conversion() == Conversion.HEXADECIMAL_FLOAT)
+            spec.conversion().fail(value);
+        StringBuilder sb = new StringBuilder();
+        boolean neg = value.signum() == -1;
+        BigDecimal v = value.abs();
+        // leading sign indicator
+        leadingSign(spec, sb, neg);
 
-            // the value
-            print(sb, v, l, f, c, precision, neg);
+        // the value
+        print(sb, v, l, spec.flags(), spec.conversion(), spec.width(), spec.precision(), neg);
 
-            // trailing sign indicator
-            trailingSign(sb, neg);
+        // trailing sign indicator
+        trailingSign(spec, sb, neg);
 
-            // justify based on width
-            appendJustified(a, sb);
-        }
+        // justify based on width
+        appendJustified(spec, a, sb);
+    }
 
-        // value > 0
-        private void print(StringBuilder sb, BigDecimal value, Locale l,
-                           Flags f, char c, int precision, boolean neg)
+    // value > 0
+    private void print(StringBuilder sb, BigDecimal value, Locale l,
+                       int f, Conversion conversion, int width, int precision, boolean neg)
             throws IOException
-        {
-            if (c == Conversion.SCIENTIFIC) {
-                // Create a new BigDecimal with the desired precision.
-                int prec = (precision == -1 ? 6 : precision);
-                int scale = value.scale();
-                int origPrec = value.precision();
-                int nzeros = 0;
-                int compPrec;
-
-                if (prec > origPrec - 1) {
-                    compPrec = origPrec;
-                    nzeros = prec - (origPrec - 1);
-                } else {
-                    compPrec = prec + 1;
-                }
+    {
+        if (conversion == Conversion.SCIENTIFIC) {
+            // Create a new BigDecimal with the desired precision.
+            int prec = (precision == -1 ? 6 : precision);
+            int scale = value.scale();
+            int origPrec = value.precision();
+            int nzeros = 0;
+            int compPrec;
+
+            if (prec > origPrec - 1) {
+                compPrec = origPrec;
+                nzeros = prec - (origPrec - 1);
+            } else {
+                compPrec = prec + 1;
+            }
 
-                MathContext mc = new MathContext(compPrec);
-                BigDecimal v
+            MathContext mc = new MathContext(compPrec);
+            BigDecimal v
                     = new BigDecimal(value.unscaledValue(), scale, mc);
 
-                BigDecimalLayout bdl
+            BigDecimalLayout bdl
                     = new BigDecimalLayout(v.unscaledValue(), v.scale(),
-                                           BigDecimalLayoutForm.SCIENTIFIC);
-
-                StringBuilder mant = bdl.mantissa();
+                    BigDecimalLayoutForm.SCIENTIFIC);
 
-                // Add a decimal point if necessary.  The mantissa may not
-                // contain a decimal point if the scale is zero (the internal
-                // representation has no fractional part) or the original
-                // precision is one. Append a decimal point if '#' is set or if
-                // we require zero padding to get to the requested precision.
-                if ((origPrec == 1 || !bdl.hasDot())
-                        && (nzeros > 0 || (f.contains(Flags.ALTERNATE)))) {
-                    mant.append('.');
-                }
+            StringBuilder mant = bdl.mantissa();
 
-                // Add trailing zeros in the case precision is greater than
-                // the number of available digits after the decimal separator.
-                trailingZeros(mant, nzeros);
-
-                StringBuilder exp = bdl.exponent();
-                int newW = width;
-                if (width != -1) {
-                    newW = adjustWidth(width - exp.length() - 1, f, neg);
-                }
-                localizedMagnitude(sb, mant, 0, f, newW, l);
+            // Add a decimal point if necessary.  The mantissa may not
+            // contain a decimal point if the scale is zero (the internal
+            // representation has no fractional part) or the original
+            // precision is one. Append a decimal point if '#' is set or if
+            // we require zero padding to get to the requested precision.
+            if ((origPrec == 1 || !bdl.hasDot())
+                    && (nzeros > 0 || (Flags.contains(f, Flags.ALTERNATE)))) {
+                mant.append('.');
+            }
 
-                sb.append(f.contains(Flags.UPPERCASE) ? 'E' : 'e');
+            // Add trailing zeros in the case precision is greater than
+            // the number of available digits after the decimal separator.
+            trailingZeros(mant, nzeros);
 
-                Flags flags = f.dup().remove(Flags.GROUP);
-                char sign = exp.charAt(0);
-                assert(sign == '+' || sign == '-');
-                sb.append(sign);
+            StringBuilder exp = bdl.exponent();
+            int newW = width;
+            if (newW != -1) {
+                newW = adjustWidth(newW - exp.length() - 1, f, neg);
+            }
+            localizedMagnitude(sb, mant, 0, f, newW, l);
 
-                sb.append(localizedMagnitude(null, exp, 1, flags, -1, l));
-            } else if (c == Conversion.DECIMAL_FLOAT) {
-                // Create a new BigDecimal with the desired precision.
-                int prec = (precision == -1 ? 6 : precision);
-                int scale = value.scale();
-
-                if (scale > prec) {
-                    // more "scale" digits than the requested "precision"
-                    int compPrec = value.precision();
-                    if (compPrec <= scale) {
-                        // case of 0.xxxxxx
-                        value = value.setScale(prec, RoundingMode.HALF_UP);
-                    } else {
-                        compPrec -= (scale - prec);
-                        value = new BigDecimal(value.unscaledValue(),
-                                               scale,
-                                               new MathContext(compPrec));
-                    }
-                }
-                BigDecimalLayout bdl = new BigDecimalLayout(
-                                           value.unscaledValue(),
-                                           value.scale(),
-                                           BigDecimalLayoutForm.DECIMAL_FLOAT);
-
-                StringBuilder mant = bdl.mantissa();
-                int nzeros = (bdl.scale() < prec ? prec - bdl.scale() : 0);
-
-                // Add a decimal point if necessary.  The mantissa may not
-                // contain a decimal point if the scale is zero (the internal
-                // representation has no fractional part).  Append a decimal
-                // point if '#' is set or we require zero padding to get to the
-                // requested precision.
-                if (bdl.scale() == 0 && (f.contains(Flags.ALTERNATE)
-                        || nzeros > 0)) {
-                    mant.append('.');
-                }
+            sb.append(Flags.contains(f, Flags.UPPERCASE) ? 'E' : 'e');
 
-                // Add trailing zeros if the precision is greater than the
-                // number of available digits after the decimal separator.
-                trailingZeros(mant, nzeros);
-
-                localizedMagnitude(sb, mant, 0, f, adjustWidth(width, f, neg), l);
-            } else if (c == Conversion.GENERAL) {
-                int prec = precision;
-                if (precision == -1)
-                    prec = 6;
-                else if (precision == 0)
-                    prec = 1;
-
-                BigDecimal tenToTheNegFour = BigDecimal.valueOf(1, 4);
-                BigDecimal tenToThePrec = BigDecimal.valueOf(1, -prec);
-                if ((value.equals(BigDecimal.ZERO))
+            int flags = Flags.remove(f, Flags.GROUP);
+            char sign = exp.charAt(0);
+            assert(sign == '+' || sign == '-');
+            sb.append(sign);
+
+            sb.append(localizedMagnitude(null, exp, 1, flags, -1, l));
+        } else if (conversion == Conversion.DECIMAL_FLOAT) {
+            // Create a new BigDecimal with the desired precision.
+            int prec = (precision == -1 ? 6 : precision);
+            int scale = value.scale();
+
+            if (scale > prec) {
+                // more "scale" digits than the requested "precision"
+                int compPrec = value.precision();
+                if (compPrec <= scale) {
+                    // case of 0.xxxxxx
+                    value = value.setScale(prec, RoundingMode.HALF_UP);
+                } else {
+                    compPrec -= (scale - prec);
+                    value = new BigDecimal(value.unscaledValue(),
+                            scale,
+                            new MathContext(compPrec));
+                }
+            }
+            BigDecimalLayout bdl = new BigDecimalLayout(
+                    value.unscaledValue(),
+                    value.scale(),
+                    BigDecimalLayoutForm.DECIMAL_FLOAT);
+
+            StringBuilder mant = bdl.mantissa();
+            int nzeros = (bdl.scale() < prec ? prec - bdl.scale() : 0);
+
+            // Add a decimal point if necessary.  The mantissa may not
+            // contain a decimal point if the scale is zero (the internal
+            // representation has no fractional part).  Append a decimal
+            // point if '#' is set or we require zero padding to get to the
+            // requested precision.
+            if (bdl.scale() == 0 && (Flags.contains(f, Flags.ALTERNATE)
+                    || nzeros > 0)) {
+                mant.append('.');
+            }
+
+            // Add trailing zeros if the precision is greater than the
+            // number of available digits after the decimal separator.
+            trailingZeros(mant, nzeros);
+
+            localizedMagnitude(sb, mant, 0, f, adjustWidth(width, f, neg), l);
+        } else if (conversion == Conversion.GENERAL) {
+            int prec = precision;
+            if (precision == -1)
+                prec = 6;
+            else if (precision == 0)
+                prec = 1;
+
+            BigDecimal tenToTheNegFour = BigDecimal.valueOf(1, 4);
+            BigDecimal tenToThePrec = BigDecimal.valueOf(1, -prec);
+            if ((value.equals(BigDecimal.ZERO))
                     || ((value.compareTo(tenToTheNegFour) != -1)
-                        && (value.compareTo(tenToThePrec) == -1))) {
+                    && (value.compareTo(tenToThePrec) == -1))) {
 
-                    int e = - value.scale()
+                int e = - value.scale()
                         + (value.unscaledValue().toString().length() - 1);
 
-                    // xxx.yyy
-                    //   g precision (# sig digits) = #x + #y
-                    //   f precision = #y
-                    //   exponent = #x - 1
-                    // => f precision = g precision - exponent - 1
-                    // 0.000zzz
-                    //   g precision (# sig digits) = #z
-                    //   f precision = #0 (after '.') + #z
-                    //   exponent = - #0 (after '.') - 1
-                    // => f precision = g precision - exponent - 1
-                    prec = prec - e - 1;
+                // xxx.yyy
+                //   g precision (# sig digits) = #x + #y
+                //   f precision = #y
+                //   exponent = #x - 1
+                // => f precision = g precision - exponent - 1
+                // 0.000zzz
+                //   g precision (# sig digits) = #z
+                //   f precision = #0 (after '.') + #z
+                //   exponent = - #0 (after '.') - 1
+                // => f precision = g precision - exponent - 1
+                prec = prec - e - 1;
 
-                    print(sb, value, l, f, Conversion.DECIMAL_FLOAT, prec,
-                          neg);
-                } else {
-                    print(sb, value, l, f, Conversion.SCIENTIFIC, prec - 1, neg);
-                }
-            } else if (c == Conversion.HEXADECIMAL_FLOAT) {
-                // This conversion isn't supported.  The error should be
-                // reported earlier.
-                assert false;
+                print(sb, value, l, f, Conversion.DECIMAL_FLOAT, width, prec, neg);
+            } else {
+                print(sb, value, l, f, Conversion.SCIENTIFIC, width, prec - 1, neg);
             }
+        } else if (conversion == Conversion.HEXADECIMAL_FLOAT) {
+            // This conversion isn't supported.  The error should be
+            // reported earlier.
+            assert false;
+        }
+    }
+
+    private class BigDecimalLayout {
+        private StringBuilder mant;
+        private StringBuilder exp;
+        private boolean dot = false;
+        private int scale;
+
+        public BigDecimalLayout(BigInteger intVal, int scale, BigDecimalLayoutForm form) {
+            layout(intVal, scale, form);
         }
 
-        private class BigDecimalLayout {
-            private StringBuilder mant;
-            private StringBuilder exp;
-            private boolean dot = false;
-            private int scale;
-
-            public BigDecimalLayout(BigInteger intVal, int scale, BigDecimalLayoutForm form) {
-                layout(intVal, scale, form);
-            }
-
-            public boolean hasDot() {
-                return dot;
-            }
-
-            public int scale() {
-                return scale;
-            }
-
-            public StringBuilder mantissa() {
-                return mant;
-            }
-
-            // The exponent will be formatted as a sign ('+' or '-') followed
-            // by the exponent zero-padded to include at least two digits.
-            public StringBuilder exponent() {
-                return exp;
-            }
-
-            private void layout(BigInteger intVal, int scale, BigDecimalLayoutForm form) {
-                String coeff = intVal.toString();
-                this.scale = scale;
-
-                // Construct a buffer, with sufficient capacity for all cases.
-                // If E-notation is needed, length will be: +1 if negative, +1
-                // if '.' needed, +2 for "E+", + up to 10 for adjusted
-                // exponent.  Otherwise it could have +1 if negative, plus
-                // leading "0.00000"
-                int len = coeff.length();
-                mant = new StringBuilder(len + 14);
-
-                if (scale == 0) {
-                    if (len > 1) {
-                        mant.append(coeff.charAt(0));
-                        if (form == BigDecimalLayoutForm.SCIENTIFIC) {
-                            mant.append('.');
-                            dot = true;
-                            mant.append(coeff, 1, len);
-                            exp = new StringBuilder("+");
-                            if (len < 10) {
-                                exp.append('0').append(len - 1);
-                            } else {
-                                exp.append(len - 1);
-                            }
+        public boolean hasDot() {
+            return dot;
+        }
+
+        public int scale() {
+            return scale;
+        }
+
+        public StringBuilder mantissa() {
+            return mant;
+        }
+
+        // The exponent will be formatted as a sign ('+' or '-') followed
+        // by the exponent zero-padded to include at least two digits.
+        public StringBuilder exponent() {
+            return exp;
+        }
+
+        private void layout(BigInteger intVal, int scale, BigDecimalLayoutForm form) {
+            String coeff = intVal.toString();
+            this.scale = scale;
+
+            // Construct a buffer, with sufficient capacity for all cases.
+            // If E-notation is needed, length will be: +1 if negative, +1
+            // if '.' needed, +2 for "E+", + up to 10 for adjusted
+            // exponent.  Otherwise it could have +1 if negative, plus
+            // leading "0.00000"
+            int len = coeff.length();
+            mant = new StringBuilder(len + 14);
+
+            if (scale == 0) {
+                if (len > 1) {
+                    mant.append(coeff.charAt(0));
+                    if (form == BigDecimalLayoutForm.SCIENTIFIC) {
+                        mant.append('.');
+                        dot = true;
+                        mant.append(coeff, 1, len);
+                        exp = new StringBuilder("+");
+                        if (len < 10) {
+                            exp.append('0').append(len - 1);
                         } else {
-                            mant.append(coeff, 1, len);
+                            exp.append(len - 1);
                         }
                     } else {
-                        mant.append(coeff);
-                        if (form == BigDecimalLayoutForm.SCIENTIFIC) {
-                            exp = new StringBuilder("+00");
-                        }
+                        mant.append(coeff, 1, len);
                     }
-                } else if (form == BigDecimalLayoutForm.DECIMAL_FLOAT) {
-                    // count of padding zeros
-
-                    if (scale >= len) {
-                        // 0.xxx form
-                        mant.append("0.");
-                        dot = true;
-                        trailingZeros(mant, scale - len);
-                        mant.append(coeff);
-                    } else {
-                        if (scale > 0) {
-                            // xx.xx form
-                            int pad = len - scale;
-                            mant.append(coeff, 0, pad);
-                            mant.append('.');
-                            dot = true;
-                            mant.append(coeff, pad, len);
-                        } else { // scale < 0
-                            // xx form
-                            mant.append(coeff, 0, len);
-                            if (intVal.signum() != 0) {
-                                trailingZeros(mant, -scale);
-                            }
-                            this.scale = 0;
-                        }
+                } else {
+                    mant.append(coeff);
+                    if (form == BigDecimalLayoutForm.SCIENTIFIC) {
+                        exp = new StringBuilder("+00");
                     }
+                }
+            } else if (form == BigDecimalLayoutForm.DECIMAL_FLOAT) {
+                // count of padding zeros
+
+                if (scale >= len) {
+                    // 0.xxx form
+                    mant.append("0.");
+                    dot = true;
+                    trailingZeros(mant, scale - len);
+                    mant.append(coeff);
                 } else {
-                    // x.xxx form
-                    mant.append(coeff.charAt(0));
-                    if (len > 1) {
+                    if (scale > 0) {
+                        // xx.xx form
+                        int pad = len - scale;
+                        mant.append(coeff, 0, pad);
                         mant.append('.');
                         dot = true;
-                        mant.append(coeff, 1, len);
-                    }
-                    exp = new StringBuilder();
-                    long adjusted = -(long) scale + (len - 1);
-                    if (adjusted != 0) {
-                        long abs = Math.abs(adjusted);
-                        // require sign
-                        exp.append(adjusted < 0 ? '-' : '+');
-                        if (abs < 10) {
-                            exp.append('0');
+                        mant.append(coeff, pad, len);
+                    } else { // scale < 0
+                        // xx form
+                        mant.append(coeff, 0, len);
+                        if (intVal.signum() != 0) {
+                            trailingZeros(mant, -scale);
                         }
-                        exp.append(abs);
-                    } else {
-                        exp.append("+00");
+                        this.scale = 0;
                     }
                 }
+            } else {
+                // x.xxx form
+                mant.append(coeff.charAt(0));
+                if (len > 1) {
+                    mant.append('.');
+                    dot = true;
+                    mant.append(coeff, 1, len);
+                }
+                exp = new StringBuilder();
+                long adjusted = -(long) scale + (len - 1);
+                if (adjusted != 0) {
+                    long abs = Math.abs(adjusted);
+                    // require sign
+                    exp.append(adjusted < 0 ? '-' : '+');
+                    if (abs < 10) {
+                        exp.append('0');
+                    }
+                    exp.append(abs);
+                } else {
+                    exp.append("+00");
+                }
             }
         }
+    }
 
-        private int adjustWidth(int width, Flags f, boolean neg) {
-            int newW = width;
-            if (newW != -1 && neg && f.contains(Flags.PARENTHESES))
-                newW--;
-            return newW;
-        }
+    private int adjustWidth(int width, int f, boolean neg) {
+        int newW = width;
+        if (newW != -1 && neg && Flags.contains(f, Flags.PARENTHESES))
+            newW--;
+        return newW;
+    }
 
-        // Add trailing zeros
-        private void trailingZeros(StringBuilder sb, int nzeros) {
-            for (int i = 0; i < nzeros; i++) {
-                sb.append('0');
-            }
+    // Add trailing zeros
+    private void trailingZeros(StringBuilder sb, int nzeros) {
+        for (int i = 0; i < nzeros; i++) {
+            sb.append('0');
         }
+    }
 
-        private void print(Calendar t, char c, Locale l)  throws IOException {
-            StringBuilder sb = new StringBuilder();
-            print(sb, t, c, l);
-
-            // justify based on width
-            if (f.contains(Flags.UPPERCASE)) {
-                appendJustified(a, toUpperCaseWithLocale(sb.toString(), l));
-            } else {
-                appendJustified(a, sb);
-            }
+    private void print(FormatSpecifier spec, Calendar t, DateTime dt, Locale l)  throws IOException {
+        StringBuilder sb = new StringBuilder();
+        print(spec, sb, t, dt, l);
+
+        // justify based on width
+        if (Flags.contains(spec.flags(), Flags.UPPERCASE)) {
+            appendJustified(spec, a, toUpperCaseWithLocale(sb.toString(), l));
+        } else {
+            appendJustified(spec, a, sb);
         }
+    }
 
-        private Appendable print(StringBuilder sb, Calendar t, char c, Locale l)
-                throws IOException {
-            if (sb == null)
-                sb = new StringBuilder();
-            switch (c) {
-            case DateTime.HOUR_OF_DAY_0: // 'H' (00 - 23)
-            case DateTime.HOUR_0:        // 'I' (01 - 12)
-            case DateTime.HOUR_OF_DAY:   // 'k' (0 - 23) -- like H
-            case DateTime.HOUR:        { // 'l' (1 - 12) -- like I
+    private Appendable print(FormatSpecifier spec, StringBuilder sb, Calendar t, DateTime dt, Locale l)
+            throws IOException {
+        if (sb == null)
+            sb = new StringBuilder();
+        switch (dt) {
+            case HOUR_OF_DAY_0: // 'H' (00 - 23)
+            case HOUR_0:        // 'I' (01 - 12)
+            case HOUR_OF_DAY:   // 'k' (0 - 23) -- like H
+            case HOUR:        { // 'l' (1 - 12) -- like I
                 int i = t.get(Calendar.HOUR_OF_DAY);
-                if (c == DateTime.HOUR_0 || c == DateTime.HOUR)
+                if (dt == DateTime.HOUR_0 || dt == DateTime.HOUR)
                     i = (i == 0 || i == 12 ? 12 : i % 12);
-                Flags flags = (c == DateTime.HOUR_OF_DAY_0
-                               || c == DateTime.HOUR_0
-                               ? Flags.ZERO_PAD
-                               : Flags.NONE);
+                int flags = (dt == DateTime.HOUR_OF_DAY_0
+                        || dt == DateTime.HOUR_0
+                        ? Flags.ZERO_PAD
+                        : Flags.NONE);
                 sb.append(localizedMagnitude(null, i, flags, 2, l));
                 break;
             }
-            case DateTime.MINUTE:      { // 'M' (00 - 59)
+            case MINUTE:      { // 'M' (00 - 59)
                 int i = t.get(Calendar.MINUTE);
-                Flags flags = Flags.ZERO_PAD;
+                int flags = Flags.ZERO_PAD;
                 sb.append(localizedMagnitude(null, i, flags, 2, l));
                 break;
             }
-            case DateTime.NANOSECOND:  { // 'N' (000000000 - 999999999)
+            case NANOSECOND:  { // 'N' (000000000 - 999999999)
                 int i = t.get(Calendar.MILLISECOND) * 1000000;
-                Flags flags = Flags.ZERO_PAD;
+                int flags = Flags.ZERO_PAD;
                 sb.append(localizedMagnitude(null, i, flags, 9, l));
                 break;
             }
-            case DateTime.MILLISECOND: { // 'L' (000 - 999)
+            case MILLISECOND: { // 'L' (000 - 999)
                 int i = t.get(Calendar.MILLISECOND);
-                Flags flags = Flags.ZERO_PAD;
+                int flags = Flags.ZERO_PAD;
                 sb.append(localizedMagnitude(null, i, flags, 3, l));
                 break;
             }
-            case DateTime.MILLISECOND_SINCE_EPOCH: { // 'Q' (0 - 99...?)
+            case MILLISECOND_SINCE_EPOCH: { // 'Q' (0 - 99...?)
                 long i = t.getTimeInMillis();
-                Flags flags = Flags.NONE;
-                sb.append(localizedMagnitude(null, i, flags, width, l));
+                int flags = Flags.NONE;
+                sb.append(localizedMagnitude(null, i, flags, spec.width(), l));
                 break;
             }
-            case DateTime.AM_PM:       { // 'p' (am or pm)
+            case AM_PM:       { // 'p' (am or pm)
                 // Calendar.AM = 0, Calendar.PM = 1, LocaleElements defines upper
                 String[] ampm = { "AM", "PM" };
                 if (l != null && l != Locale.US) {
                     DateFormatSymbols dfs = DateFormatSymbols.getInstance(l);
                     ampm = dfs.getAmPmStrings();
                 }
                 String s = ampm[t.get(Calendar.AM_PM)];
                 sb.append(s.toLowerCase(Objects.requireNonNullElse(l,
-                            Locale.getDefault(Locale.Category.FORMAT))));
+                        Locale.getDefault(Locale.Category.FORMAT))));
                 break;
             }
-            case DateTime.SECONDS_SINCE_EPOCH: { // 's' (0 - 99...?)
+            case SECONDS_SINCE_EPOCH: { // 's' (0 - 99...?)
                 long i = t.getTimeInMillis() / 1000;
-                Flags flags = Flags.NONE;
-                sb.append(localizedMagnitude(null, i, flags, width, l));
+                int flags = Flags.NONE;
+                sb.append(localizedMagnitude(null, i, flags, spec.width(), l));
                 break;
             }
-            case DateTime.SECOND:      { // 'S' (00 - 60 - leap second)
+            case SECOND:      { // 'S' (00 - 60 - leap second)
                 int i = t.get(Calendar.SECOND);
-                Flags flags = Flags.ZERO_PAD;
+                int flags = Flags.ZERO_PAD;
                 sb.append(localizedMagnitude(null, i, flags, 2, l));
                 break;
             }
-            case DateTime.ZONE_NUMERIC: { // 'z' ({-|+}####) - ls minus?
+            case ZONE_NUMERIC: { // 'z' ({-|+}####) - ls minus?
                 int i = t.get(Calendar.ZONE_OFFSET) + t.get(Calendar.DST_OFFSET);
                 boolean neg = i < 0;
                 sb.append(neg ? '-' : '+');
                 if (neg)
                     i = -i;
                 int min = i / 60000;
                 // combine minute and hour into a single integer
                 int offset = (min / 60) * 100 + (min % 60);
-                Flags flags = Flags.ZERO_PAD;
+                int flags = Flags.ZERO_PAD;
 
                 sb.append(localizedMagnitude(null, offset, flags, 4, l));
                 break;
             }
-            case DateTime.ZONE:        { // 'Z' (symbol)
+            case ZONE:        { // 'Z' (symbol)
                 TimeZone tz = t.getTimeZone();
                 sb.append(tz.getDisplayName((t.get(Calendar.DST_OFFSET) != 0),
-                                           TimeZone.SHORT,
-                                           Objects.requireNonNullElse(l, Locale.US)));
+                        TimeZone.SHORT,
+                        Objects.requireNonNullElse(l, Locale.US)));
                 break;
             }
 
             // Date
-            case DateTime.NAME_OF_DAY_ABBREV:     // 'a'
-            case DateTime.NAME_OF_DAY:          { // 'A'
+            case NAME_OF_DAY_ABBREV:     // 'a'
+            case NAME_OF_DAY:          { // 'A'
                 int i = t.get(Calendar.DAY_OF_WEEK);
                 Locale lt = Objects.requireNonNullElse(l, Locale.US);
                 DateFormatSymbols dfs = DateFormatSymbols.getInstance(lt);
-                if (c == DateTime.NAME_OF_DAY)
+                if (dt == DateTime.NAME_OF_DAY)
                     sb.append(dfs.getWeekdays()[i]);
                 else
                     sb.append(dfs.getShortWeekdays()[i]);
                 break;
             }
-            case DateTime.NAME_OF_MONTH_ABBREV:   // 'b'
-            case DateTime.NAME_OF_MONTH_ABBREV_X: // 'h' -- same b
-            case DateTime.NAME_OF_MONTH:        { // 'B'
+            case NAME_OF_MONTH_ABBREV:   // 'b'
+            case NAME_OF_MONTH_ABBREV_X: // 'h' -- same b
+            case NAME_OF_MONTH:        { // 'B'
                 int i = t.get(Calendar.MONTH);
                 Locale lt = Objects.requireNonNullElse(l, Locale.US);
                 DateFormatSymbols dfs = DateFormatSymbols.getInstance(lt);
-                if (c == DateTime.NAME_OF_MONTH)
+                if (dt == DateTime.NAME_OF_MONTH)
                     sb.append(dfs.getMonths()[i]);
                 else
                     sb.append(dfs.getShortMonths()[i]);
                 break;
             }
-            case DateTime.CENTURY:                // 'C' (00 - 99)
-            case DateTime.YEAR_2:                 // 'y' (00 - 99)
-            case DateTime.YEAR_4:               { // 'Y' (0000 - 9999)
+            case CENTURY:                // 'C' (00 - 99)
+            case YEAR_2:                 // 'y' (00 - 99)
+            case YEAR_4:               { // 'Y' (0000 - 9999)
                 int i = t.get(Calendar.YEAR);
                 int size = 2;
-                switch (c) {
-                case DateTime.CENTURY:
-                    i /= 100;
-                    break;
-                case DateTime.YEAR_2:
-                    i %= 100;
-                    break;
-                case DateTime.YEAR_4:
-                    size = 4;
-                    break;
+                switch (dt) {
+                    case CENTURY:
+                        i /= 100;
+                        break;
+                    case YEAR_2:
+                        i %= 100;
+                        break;
+                    case YEAR_4:
+                        size = 4;
+                        break;
                 }
-                Flags flags = Flags.ZERO_PAD;
+                int flags = Flags.ZERO_PAD;
                 sb.append(localizedMagnitude(null, i, flags, size, l));
                 break;
             }
-            case DateTime.DAY_OF_MONTH_0:         // 'd' (01 - 31)
-            case DateTime.DAY_OF_MONTH:         { // 'e' (1 - 31) -- like d
+            case DAY_OF_MONTH_0:         // 'd' (01 - 31)
+            case DAY_OF_MONTH:         { // 'e' (1 - 31) -- like d
                 int i = t.get(Calendar.DATE);
-                Flags flags = (c == DateTime.DAY_OF_MONTH_0
-                               ? Flags.ZERO_PAD
-                               : Flags.NONE);
+                int flags = (dt == DateTime.DAY_OF_MONTH_0
+                        ? Flags.ZERO_PAD
+                        : Flags.NONE);
                 sb.append(localizedMagnitude(null, i, flags, 2, l));
                 break;
             }
-            case DateTime.DAY_OF_YEAR:          { // 'j' (001 - 366)
+            case DAY_OF_YEAR:          { // 'j' (001 - 366)
                 int i = t.get(Calendar.DAY_OF_YEAR);
-                Flags flags = Flags.ZERO_PAD;
+                int flags = Flags.ZERO_PAD;
                 sb.append(localizedMagnitude(null, i, flags, 3, l));
                 break;
             }
-            case DateTime.MONTH:                { // 'm' (01 - 12)
+            case MONTH:                { // 'm' (01 - 12)
                 int i = t.get(Calendar.MONTH) + 1;
-                Flags flags = Flags.ZERO_PAD;
+                int flags = Flags.ZERO_PAD;
                 sb.append(localizedMagnitude(null, i, flags, 2, l));
                 break;
             }
 
             // Composites
-            case DateTime.TIME:         // 'T' (24 hour hh:mm:ss - %tH:%tM:%tS)
-            case DateTime.TIME_24_HOUR:    { // 'R' (hh:mm same as %H:%M)
+            case TIME:         // 'T' (24 hour hh:mm:ss - %tH:%tM:%tS)
+            case TIME_24_HOUR:    { // 'R' (hh:mm same as %H:%M)
                 char sep = ':';
-                print(sb, t, DateTime.HOUR_OF_DAY_0, l).append(sep);
-                print(sb, t, DateTime.MINUTE, l);
-                if (c == DateTime.TIME) {
+                print(spec, sb, t, DateTime.HOUR_OF_DAY_0, l).append(sep);
+                print(spec, sb, t, DateTime.MINUTE, l);
+                if (dt == DateTime.TIME) {
                     sb.append(sep);
-                    print(sb, t, DateTime.SECOND, l);
+                    print(spec, sb, t, DateTime.SECOND, l);
                 }
                 break;
             }
-            case DateTime.TIME_12_HOUR:    { // 'r' (hh:mm:ss [AP]M)
+            case TIME_12_HOUR:    { // 'r' (hh:mm:ss [AP]M)
                 char sep = ':';
-                print(sb, t, DateTime.HOUR_0, l).append(sep);
-                print(sb, t, DateTime.MINUTE, l).append(sep);
-                print(sb, t, DateTime.SECOND, l).append(' ');
+                print(spec, sb, t, DateTime.HOUR_0, l).append(sep);
+                print(spec, sb, t, DateTime.MINUTE, l).append(sep);
+                print(spec, sb, t, DateTime.SECOND, l).append(' ');
                 // this may be in wrong place for some locales
                 StringBuilder tsb = new StringBuilder();
-                print(tsb, t, DateTime.AM_PM, l);
+                print(spec, tsb, t, DateTime.AM_PM, l);
 
                 sb.append(toUpperCaseWithLocale(tsb.toString(), l));
                 break;
             }
-            case DateTime.DATE_TIME:    { // 'c' (Sat Nov 04 12:02:33 EST 1999)
+            case DATE_TIME:    { // 'c' (Sat Nov 04 12:02:33 EST 1999)
                 char sep = ' ';
-                print(sb, t, DateTime.NAME_OF_DAY_ABBREV, l).append(sep);
-                print(sb, t, DateTime.NAME_OF_MONTH_ABBREV, l).append(sep);
-                print(sb, t, DateTime.DAY_OF_MONTH_0, l).append(sep);
-                print(sb, t, DateTime.TIME, l).append(sep);
-                print(sb, t, DateTime.ZONE, l).append(sep);
-                print(sb, t, DateTime.YEAR_4, l);
+                print(spec, sb, t, DateTime.NAME_OF_DAY_ABBREV, l).append(sep);
+                print(spec, sb, t, DateTime.NAME_OF_MONTH_ABBREV, l).append(sep);
+                print(spec, sb, t, DateTime.DAY_OF_MONTH_0, l).append(sep);
+                print(spec, sb, t, DateTime.TIME, l).append(sep);
+                print(spec, sb, t, DateTime.ZONE, l).append(sep);
+                print(spec, sb, t, DateTime.YEAR_4, l);
                 break;
             }
-            case DateTime.DATE:            { // 'D' (mm/dd/yy)
+            case DATE:            { // 'D' (mm/dd/yy)
                 char sep = '/';
-                print(sb, t, DateTime.MONTH, l).append(sep);
-                print(sb, t, DateTime.DAY_OF_MONTH_0, l).append(sep);
-                print(sb, t, DateTime.YEAR_2, l);
+                print(spec, sb, t, DateTime.MONTH, l).append(sep);
+                print(spec, sb, t, DateTime.DAY_OF_MONTH_0, l).append(sep);
+                print(spec, sb, t, DateTime.YEAR_2, l);
                 break;
             }
-            case DateTime.ISO_STANDARD_DATE: { // 'F' (%Y-%m-%d)
+            case ISO_STANDARD_DATE: { // 'F' (%Y-%m-%d)
                 char sep = '-';
-                print(sb, t, DateTime.YEAR_4, l).append(sep);
-                print(sb, t, DateTime.MONTH, l).append(sep);
-                print(sb, t, DateTime.DAY_OF_MONTH_0, l);
+                print(spec, sb, t, DateTime.YEAR_4, l).append(sep);
+                print(spec, sb, t, DateTime.MONTH, l).append(sep);
+                print(spec, sb, t, DateTime.DAY_OF_MONTH_0, l);
                 break;
             }
             default:
                 assert false;
-            }
-            return sb;
         }
+        return sb;
+    }
 
-        private void print(TemporalAccessor t, char c, Locale l)  throws IOException {
-            StringBuilder sb = new StringBuilder();
-            print(sb, t, c, l);
-            // justify based on width
-            if (f.contains(Flags.UPPERCASE)) {
-                appendJustified(a, toUpperCaseWithLocale(sb.toString(), l));
-            } else {
-                appendJustified(a, sb);
-            }
+    private void print(FormatSpecifier spec, TemporalAccessor t, DateTime dt, Locale l)
+            throws IOException {
+        StringBuilder sb = new StringBuilder();
+        print(spec, sb, t, dt, l);
+        // justify based on width
+        if (Flags.contains(spec.flags(), Flags.UPPERCASE)) {
+            appendJustified(spec, a, toUpperCaseWithLocale(sb.toString(), l));
+        } else {
+            appendJustified(spec, a, sb);
         }
+    }
 
-        private Appendable print(StringBuilder sb, TemporalAccessor t, char c,
-                                 Locale l) throws IOException {
-            if (sb == null)
-                sb = new StringBuilder();
-            try {
-                switch (c) {
-                case DateTime.HOUR_OF_DAY_0: {  // 'H' (00 - 23)
+    private Appendable print(FormatSpecifier spec, StringBuilder sb, TemporalAccessor t,
+                             DateTime dt, Locale l) throws IOException {
+        if (sb == null)
+            sb = new StringBuilder();
+        try {
+            switch (dt) {
+                case HOUR_OF_DAY_0: {  // 'H' (00 - 23)
                     int i = t.get(ChronoField.HOUR_OF_DAY);
                     sb.append(localizedMagnitude(null, i, Flags.ZERO_PAD, 2, l));
                     break;
                 }
-                case DateTime.HOUR_OF_DAY: {   // 'k' (0 - 23) -- like H
+                case HOUR_OF_DAY: {   // 'k' (0 - 23) -- like H
                     int i = t.get(ChronoField.HOUR_OF_DAY);
                     sb.append(localizedMagnitude(null, i, Flags.NONE, 2, l));
                     break;
                 }
-                case DateTime.HOUR_0:      {  // 'I' (01 - 12)
+                case HOUR_0:      {  // 'I' (01 - 12)
                     int i = t.get(ChronoField.CLOCK_HOUR_OF_AMPM);
                     sb.append(localizedMagnitude(null, i, Flags.ZERO_PAD, 2, l));
                     break;
                 }
-                case DateTime.HOUR:        { // 'l' (1 - 12) -- like I
+                case HOUR:        { // 'l' (1 - 12) -- like I
                     int i = t.get(ChronoField.CLOCK_HOUR_OF_AMPM);
                     sb.append(localizedMagnitude(null, i, Flags.NONE, 2, l));
                     break;
                 }
-                case DateTime.MINUTE:      { // 'M' (00 - 59)
+                case MINUTE:      { // 'M' (00 - 59)
                     int i = t.get(ChronoField.MINUTE_OF_HOUR);
-                    Flags flags = Flags.ZERO_PAD;
+                    int flags = Flags.ZERO_PAD;
                     sb.append(localizedMagnitude(null, i, flags, 2, l));
                     break;
                 }
-                case DateTime.NANOSECOND:  { // 'N' (000000000 - 999999999)
+                case NANOSECOND:  { // 'N' (000000000 - 999999999)
                     int i;
                     try {
                         i = t.get(ChronoField.NANO_OF_SECOND);
                     } catch (UnsupportedTemporalTypeException u) {
                         i = t.get(ChronoField.MILLI_OF_SECOND) * 1000000;
                     }
-                    Flags flags = Flags.ZERO_PAD;
+                    int flags = Flags.ZERO_PAD;
                     sb.append(localizedMagnitude(null, i, flags, 9, l));
                     break;
                 }
-                case DateTime.MILLISECOND: { // 'L' (000 - 999)
+                case MILLISECOND: { // 'L' (000 - 999)
                     int i = t.get(ChronoField.MILLI_OF_SECOND);
-                    Flags flags = Flags.ZERO_PAD;
+                    int flags = Flags.ZERO_PAD;
                     sb.append(localizedMagnitude(null, i, flags, 3, l));
                     break;
                 }
-                case DateTime.MILLISECOND_SINCE_EPOCH: { // 'Q' (0 - 99...?)
+                case MILLISECOND_SINCE_EPOCH: { // 'Q' (0 - 99...?)
                     long i = t.getLong(ChronoField.INSTANT_SECONDS) * 1000L +
-                             t.getLong(ChronoField.MILLI_OF_SECOND);
-                    Flags flags = Flags.NONE;
-                    sb.append(localizedMagnitude(null, i, flags, width, l));
+                            t.getLong(ChronoField.MILLI_OF_SECOND);
+                    int flags = Flags.NONE;
+                    sb.append(localizedMagnitude(null, i, flags, spec.width(), l));
                     break;
                 }
-                case DateTime.AM_PM:       { // 'p' (am or pm)
+                case AM_PM:       { // 'p' (am or pm)
                     // Calendar.AM = 0, Calendar.PM = 1, LocaleElements defines upper
                     String[] ampm = { "AM", "PM" };
                     if (l != null && l != Locale.US) {
                         DateFormatSymbols dfs = DateFormatSymbols.getInstance(l);
                         ampm = dfs.getAmPmStrings();

@@ -4249,612 +3945,1688 @@
                     String s = ampm[t.get(ChronoField.AMPM_OF_DAY)];
                     sb.append(s.toLowerCase(Objects.requireNonNullElse(l,
                             Locale.getDefault(Locale.Category.FORMAT))));
                     break;
                 }
-                case DateTime.SECONDS_SINCE_EPOCH: { // 's' (0 - 99...?)
+                case SECONDS_SINCE_EPOCH: { // 's' (0 - 99...?)
                     long i = t.getLong(ChronoField.INSTANT_SECONDS);
-                    Flags flags = Flags.NONE;
-                    sb.append(localizedMagnitude(null, i, flags, width, l));
+                    int flags = Flags.NONE;
+                    sb.append(localizedMagnitude(null, i, flags, spec.width(), l));
                     break;
                 }
-                case DateTime.SECOND:      { // 'S' (00 - 60 - leap second)
+                case SECOND:      { // 'S' (00 - 60 - leap second)
                     int i = t.get(ChronoField.SECOND_OF_MINUTE);
-                    Flags flags = Flags.ZERO_PAD;
+                    int flags = Flags.ZERO_PAD;
                     sb.append(localizedMagnitude(null, i, flags, 2, l));
                     break;
                 }
-                case DateTime.ZONE_NUMERIC: { // 'z' ({-|+}####) - ls minus?
+                case ZONE_NUMERIC: { // 'z' ({-|+}####) - ls minus?
                     int i = t.get(ChronoField.OFFSET_SECONDS);
                     boolean neg = i < 0;
                     sb.append(neg ? '-' : '+');
                     if (neg)
                         i = -i;
                     int min = i / 60;
                     // combine minute and hour into a single integer
                     int offset = (min / 60) * 100 + (min % 60);
-                    Flags flags = Flags.ZERO_PAD;
+                    int flags = Flags.ZERO_PAD;
                     sb.append(localizedMagnitude(null, offset, flags, 4, l));
                     break;
                 }
-                case DateTime.ZONE:        { // 'Z' (symbol)
+                case ZONE:        { // 'Z' (symbol)
                     ZoneId zid = t.query(TemporalQueries.zone());
                     if (zid == null) {
-                        throw new IllegalFormatConversionException(c, t.getClass());
+                        dt.fail(t);
                     }
                     if (!(zid instanceof ZoneOffset) &&
-                        t.isSupported(ChronoField.INSTANT_SECONDS)) {
+                            t.isSupported(ChronoField.INSTANT_SECONDS)) {
                         Instant instant = Instant.from(t);
                         sb.append(TimeZone.getTimeZone(zid.getId())
-                                          .getDisplayName(zid.getRules().isDaylightSavings(instant),
-                                                          TimeZone.SHORT,
-                                                          Objects.requireNonNullElse(l, Locale.US)));
+                                .getDisplayName(zid.getRules().isDaylightSavings(instant),
+                                        TimeZone.SHORT,
+                                        Objects.requireNonNullElse(l, Locale.US)));
                         break;
                     }
                     sb.append(zid.getId());
                     break;
                 }
                 // Date
-                case DateTime.NAME_OF_DAY_ABBREV:     // 'a'
-                case DateTime.NAME_OF_DAY:          { // 'A'
+                case NAME_OF_DAY_ABBREV:     // 'a'
+                case NAME_OF_DAY:          { // 'A'
                     int i = t.get(ChronoField.DAY_OF_WEEK) % 7 + 1;
                     Locale lt = Objects.requireNonNullElse(l, Locale.US);
                     DateFormatSymbols dfs = DateFormatSymbols.getInstance(lt);
-                    if (c == DateTime.NAME_OF_DAY)
+                    if (dt == DateTime.NAME_OF_DAY)
                         sb.append(dfs.getWeekdays()[i]);
                     else
                         sb.append(dfs.getShortWeekdays()[i]);
                     break;
                 }
-                case DateTime.NAME_OF_MONTH_ABBREV:   // 'b'
-                case DateTime.NAME_OF_MONTH_ABBREV_X: // 'h' -- same b
-                case DateTime.NAME_OF_MONTH:        { // 'B'
+                case NAME_OF_MONTH_ABBREV:   // 'b'
+                case NAME_OF_MONTH_ABBREV_X: // 'h' -- same b
+                case NAME_OF_MONTH:        { // 'B'
                     int i = t.get(ChronoField.MONTH_OF_YEAR) - 1;
                     Locale lt = Objects.requireNonNullElse(l, Locale.US);
                     DateFormatSymbols dfs = DateFormatSymbols.getInstance(lt);
-                    if (c == DateTime.NAME_OF_MONTH)
+                    if (dt == DateTime.NAME_OF_MONTH)
                         sb.append(dfs.getMonths()[i]);
                     else
                         sb.append(dfs.getShortMonths()[i]);
                     break;
                 }
-                case DateTime.CENTURY:                // 'C' (00 - 99)
-                case DateTime.YEAR_2:                 // 'y' (00 - 99)
-                case DateTime.YEAR_4:               { // 'Y' (0000 - 9999)
+                case CENTURY:                // 'C' (00 - 99)
+                case YEAR_2:                 // 'y' (00 - 99)
+                case YEAR_4:               { // 'Y' (0000 - 9999)
                     int i = t.get(ChronoField.YEAR_OF_ERA);
                     int size = 2;
-                    switch (c) {
-                    case DateTime.CENTURY:
-                        i /= 100;
-                        break;
-                    case DateTime.YEAR_2:
-                        i %= 100;
-                        break;
-                    case DateTime.YEAR_4:
-                        size = 4;
-                        break;
+                    switch (dt) {
+                        case CENTURY:
+                            i /= 100;
+                            break;
+                        case YEAR_2:
+                            i %= 100;
+                            break;
+                        case YEAR_4:
+                            size = 4;
+                            break;
                     }
-                    Flags flags = Flags.ZERO_PAD;
+                    int flags = Flags.ZERO_PAD;
                     sb.append(localizedMagnitude(null, i, flags, size, l));
                     break;
                 }
-                case DateTime.DAY_OF_MONTH_0:         // 'd' (01 - 31)
-                case DateTime.DAY_OF_MONTH:         { // 'e' (1 - 31) -- like d
+                case DAY_OF_MONTH_0:         // 'd' (01 - 31)
+                case DAY_OF_MONTH:         { // 'e' (1 - 31) -- like d
                     int i = t.get(ChronoField.DAY_OF_MONTH);
-                    Flags flags = (c == DateTime.DAY_OF_MONTH_0
-                                   ? Flags.ZERO_PAD
-                                   : Flags.NONE);
+                    int flags = (dt == DateTime.DAY_OF_MONTH_0
+                            ? Flags.ZERO_PAD
+                            : Flags.NONE);
                     sb.append(localizedMagnitude(null, i, flags, 2, l));
                     break;
                 }
-                case DateTime.DAY_OF_YEAR:          { // 'j' (001 - 366)
+                case DAY_OF_YEAR:          { // 'j' (001 - 366)
                     int i = t.get(ChronoField.DAY_OF_YEAR);
-                    Flags flags = Flags.ZERO_PAD;
+                    int flags = Flags.ZERO_PAD;
                     sb.append(localizedMagnitude(null, i, flags, 3, l));
                     break;
                 }
-                case DateTime.MONTH:                { // 'm' (01 - 12)
+                case MONTH:                { // 'm' (01 - 12)
                     int i = t.get(ChronoField.MONTH_OF_YEAR);
-                    Flags flags = Flags.ZERO_PAD;
+                    int flags = Flags.ZERO_PAD;
                     sb.append(localizedMagnitude(null, i, flags, 2, l));
                     break;
                 }
 
                 // Composites
-                case DateTime.TIME:         // 'T' (24 hour hh:mm:ss - %tH:%tM:%tS)
-                case DateTime.TIME_24_HOUR:    { // 'R' (hh:mm same as %H:%M)
+                case TIME:         // 'T' (24 hour hh:mm:ss - %tH:%tM:%tS)
+                case TIME_24_HOUR:    { // 'R' (hh:mm same as %H:%M)
                     char sep = ':';
-                    print(sb, t, DateTime.HOUR_OF_DAY_0, l).append(sep);
-                    print(sb, t, DateTime.MINUTE, l);
-                    if (c == DateTime.TIME) {
+                    print(spec, sb, t, DateTime.HOUR_OF_DAY_0, l).append(sep);
+                    print(spec, sb, t, DateTime.MINUTE, l);
+                    if (dt == DateTime.TIME) {
                         sb.append(sep);
-                        print(sb, t, DateTime.SECOND, l);
+                        print(spec, sb, t, DateTime.SECOND, l);
                     }
                     break;
                 }
-                case DateTime.TIME_12_HOUR:    { // 'r' (hh:mm:ss [AP]M)
+                case TIME_12_HOUR:    { // 'r' (hh:mm:ss [AP]M)
                     char sep = ':';
-                    print(sb, t, DateTime.HOUR_0, l).append(sep);
-                    print(sb, t, DateTime.MINUTE, l).append(sep);
-                    print(sb, t, DateTime.SECOND, l).append(' ');
+                    print(spec, sb, t, DateTime.HOUR_0, l).append(sep);
+                    print(spec, sb, t, DateTime.MINUTE, l).append(sep);
+                    print(spec, sb, t, DateTime.SECOND, l).append(' ');
                     // this may be in wrong place for some locales
                     StringBuilder tsb = new StringBuilder();
-                    print(tsb, t, DateTime.AM_PM, l);
+                    print(spec, tsb, t, DateTime.AM_PM, l);
                     sb.append(toUpperCaseWithLocale(tsb.toString(), l));
                     break;
                 }
-                case DateTime.DATE_TIME:    { // 'c' (Sat Nov 04 12:02:33 EST 1999)
+                case DATE_TIME:    { // 'c' (Sat Nov 04 12:02:33 EST 1999)
                     char sep = ' ';
-                    print(sb, t, DateTime.NAME_OF_DAY_ABBREV, l).append(sep);
-                    print(sb, t, DateTime.NAME_OF_MONTH_ABBREV, l).append(sep);
-                    print(sb, t, DateTime.DAY_OF_MONTH_0, l).append(sep);
-                    print(sb, t, DateTime.TIME, l).append(sep);
-                    print(sb, t, DateTime.ZONE, l).append(sep);
-                    print(sb, t, DateTime.YEAR_4, l);
+                    print(spec, sb, t, DateTime.NAME_OF_DAY_ABBREV, l).append(sep);
+                    print(spec, sb, t, DateTime.NAME_OF_MONTH_ABBREV, l).append(sep);
+                    print(spec, sb, t, DateTime.DAY_OF_MONTH_0, l).append(sep);
+                    print(spec, sb, t, DateTime.TIME, l).append(sep);
+                    print(spec, sb, t, DateTime.ZONE, l).append(sep);
+                    print(spec, sb, t, DateTime.YEAR_4, l);
                     break;
                 }
-                case DateTime.DATE:            { // 'D' (mm/dd/yy)
+                case DATE:            { // 'D' (mm/dd/yy)
                     char sep = '/';
-                    print(sb, t, DateTime.MONTH, l).append(sep);
-                    print(sb, t, DateTime.DAY_OF_MONTH_0, l).append(sep);
-                    print(sb, t, DateTime.YEAR_2, l);
+                    print(spec, sb, t, DateTime.MONTH, l).append(sep);
+                    print(spec, sb, t, DateTime.DAY_OF_MONTH_0, l).append(sep);
+                    print(spec, sb, t, DateTime.YEAR_2, l);
                     break;
                 }
-                case DateTime.ISO_STANDARD_DATE: { // 'F' (%Y-%m-%d)
+                case ISO_STANDARD_DATE: { // 'F' (%Y-%m-%d)
                     char sep = '-';
-                    print(sb, t, DateTime.YEAR_4, l).append(sep);
-                    print(sb, t, DateTime.MONTH, l).append(sep);
-                    print(sb, t, DateTime.DAY_OF_MONTH_0, l);
+                    print(spec, sb, t, DateTime.YEAR_4, l).append(sep);
+                    print(spec, sb, t, DateTime.MONTH, l).append(sep);
+                    print(spec, sb, t, DateTime.DAY_OF_MONTH_0, l);
                     break;
                 }
                 default:
                     assert false;
-                }
-            } catch (DateTimeException x) {
-                throw new IllegalFormatConversionException(c, t.getClass());
             }
-            return sb;
+        } catch (DateTimeException x) {
+            spec.conversion().fail(t);
         }
+        return sb;
+    }
 
-        // -- Methods to support throwing exceptions --
+    // -- Methods to support throwing exceptions --
 
-        private void failMismatch(Flags f, char c) {
-            String fs = f.toString();
-            throw new FormatFlagsConversionMismatchException(fs, c);
+    private void failMismatch(int flags, char c) {
+        String fs = Flags.toString(flags);
+        throw new FormatFlagsConversionMismatchException(fs, c);
+    }
+
+    private static char getZero(Formatter fmt, Locale l) {
+        if (l != null && !l.equals(fmt.locale())) {
+            DecimalFormatSymbols dfs = DecimalFormatSymbols.getInstance(l);
+            return dfs.getZeroDigit();
         }
+        return fmt.zero;
+    }
 
-        private void failConversion(char c, Object arg) {
-            throw new IllegalFormatConversionException(c, arg.getClass());
+    private StringBuilder localizedMagnitude(StringBuilder sb, long value, int flags, int width, Locale l) {
+        return localizedMagnitude(sb, Long.toString(value, 10), 0, flags, width, l);
+    }
+
+    StringBuilder localizedMagnitude(StringBuilder sb,
+                                     CharSequence value, final int offset, int flags,
+                                     int width, Locale l) {
+        if (sb == null) {
+            sb = new StringBuilder();
+        }
+        int begin = sb.length();
+
+        char zero = getZero(this, l);
+
+        // determine localized grouping separator and size
+        char grpSep = '\0';
+        int  grpSize = -1;
+        char decSep = '\0';
+
+        int len = value.length();
+        int dot = len;
+        for (int j = offset; j < len; j++) {
+            if (value.charAt(j) == '.') {
+                dot = j;
+                break;
+            }
         }
 
-        private char getZero(Locale l) {
-            if ((l != null) &&  !l.equals(locale())) {
+        if (dot < len) {
+            if (l == null || l.equals(Locale.US)) {
+                decSep  = '.';
+            } else {
                 DecimalFormatSymbols dfs = DecimalFormatSymbols.getInstance(l);
-                return dfs.getZeroDigit();
+                decSep  = dfs.getDecimalSeparator();
             }
-            return zero;
         }
 
-        private StringBuilder localizedMagnitude(StringBuilder sb,
-                long value, Flags f, int width, Locale l) {
-            return localizedMagnitude(sb, Long.toString(value, 10), 0, f, width, l);
+        if (Flags.contains(flags, Flags.GROUP)) {
+            if (l == null || l.equals(Locale.US)) {
+                grpSep = ',';
+                grpSize = 3;
+            } else {
+                DecimalFormatSymbols dfs = DecimalFormatSymbols.getInstance(l);
+                grpSep = dfs.getGroupingSeparator();
+                DecimalFormat df = null;
+                NumberFormat nf = NumberFormat.getNumberInstance(l);
+                if (nf instanceof DecimalFormat) {
+                    df = (DecimalFormat) nf;
+                } else {
+
+                    // Use DecimalFormat constructor to obtain the instance,
+                    // in case NumberFormat.getNumberInstance(l)
+                    // returns instance other than DecimalFormat
+                    LocaleProviderAdapter adapter = LocaleProviderAdapter
+                            .getAdapter(NumberFormatProvider.class, l);
+                    if (!(adapter instanceof ResourceBundleBasedAdapter)) {
+                        adapter = LocaleProviderAdapter.getResourceBundleBased();
+                    }
+                    String[] all = adapter.getLocaleResources(l)
+                            .getNumberPatterns();
+                    df = new DecimalFormat(all[0], dfs);
+                }
+                grpSize = df.getGroupingSize();
+                // Some locales do not use grouping (the number
+                // pattern for these locales does not contain group, e.g.
+                // ("#0.###")), but specify a grouping separator.
+                // To avoid unnecessary identification of the position of
+                // grouping separator, reset its value with null character
+                if (!df.isGroupingUsed() || grpSize == 0) {
+                    grpSep = '\0';
+                }
+            }
         }
 
-        private StringBuilder localizedMagnitude(StringBuilder sb,
-                CharSequence value, final int offset, Flags f, int width,
-                Locale l) {
-            if (sb == null) {
-                sb = new StringBuilder();
+        // localize the digits inserting group separators as necessary
+        for (int j = offset; j < len; j++) {
+            if (j == dot) {
+                sb.append(decSep);
+                // no more group separators after the decimal separator
+                grpSep = '\0';
+                continue;
             }
-            int begin = sb.length();
 
-            char zero = getZero(l);
+            char c = value.charAt(j);
+            sb.append((char) ((c - '0') + zero));
+            if (grpSep != '\0' && j != dot - 1 && ((dot - j) % grpSize == 1)) {
+                sb.append(grpSep);
+            }
+        }
 
-            // determine localized grouping separator and size
-            char grpSep = '\0';
-            int  grpSize = -1;
-            char decSep = '\0';
+        // apply zero padding
+        if (width != -1 && Flags.contains(flags, Flags.ZERO_PAD)) {
+            for (int k = sb.length(); k < width; k++) {
+                sb.insert(begin, zero);
+            }
+        }
 
-            int len = value.length();
-            int dot = len;
-            for (int j = offset; j < len; j++) {
-                if (value.charAt(j) == '.') {
-                    dot = j;
-                    break;
+        return sb;
+    }
+
+    // Specialized localization of exponents, where the source value can only
+    // contain characters '0' through '9', starting at index offset, and no
+    // group separators is added for any locale.
+    private void localizedMagnitudeExp(FormatSpecifier spec, StringBuilder sb, char[] value,
+                                       final int offset, Locale l) {
+        char zero = getZero(this, l);
+
+        int len = value.length;
+        for (int j = offset; j < len; j++) {
+            char c = value[j];
+            sb.append((char) ((c - '0') + zero));
+        }
+    }
+
+
+    /**
+     * Enum for {@code BigDecimal} formatting.
+     */
+    public enum BigDecimalLayoutForm {
+        /**
+         * Format the {@code BigDecimal} in computerized scientific notation.
+         */
+        SCIENTIFIC,
+
+        /**
+         * Format the {@code BigDecimal} as a decimal number.
+         */
+        DECIMAL_FLOAT
+    };
+
+    // refactoring to provide additional tools to the Formatter BSM
+    // %[argument_index$][flags][width][.precision][t]conversion
+    private static final String formatSpecifier
+            = "%(\\d+\\$)?([-#+ 0,(\\<]*)?(\\d+)?(\\.\\d+)?([tT])?([a-zA-Z%])";
+
+    private static Pattern fsPattern = Pattern.compile(formatSpecifier);
+
+    /**
+     * Creates a parsed format list.
+     * @param format to parse
+     */
+    private static List<FormatToken> parse(String format) {
+        ArrayList<FormatToken> al = new ArrayList<>();
+        Matcher m = fsPattern.matcher(format);
+        for (int i = 0, len = format.length(); i < len; ) {
+            if (m.find(i)) {
+                // Anything between the start of the string and the beginning
+                // of the format specifier is either fixed text or contains
+                // an invalid format string.
+                if (m.start() != i) {
+                    // Make sure we didn't miss any invalid format specifiers
+                    checkText(format, i, m.start());
+                    // Assume previous characters were fixed text
+                    al.add(new FixedString(format, i, m.start()));
                 }
+
+                al.add(new FormatSpecifier(format, m));
+                i = m.end();
+            } else {
+                // No more valid format specifiers.  Check for possible invalid
+                // format specifiers.
+                checkText(format, i, len);
+                // The rest of the string is fixed text
+                al.add(new FixedString(format, i, format.length()));
+                break;
             }
+        }
+        return al;
+    }
 
-            if (dot < len) {
-                if (l == null || l.equals(Locale.US)) {
-                    decSep  = '.';
-                } else {
-                    DecimalFormatSymbols dfs = DecimalFormatSymbols.getInstance(l);
-                    decSep  = dfs.getDecimalSeparator();
-                }
+    private static void checkText(String s, int start, int end) {
+        for (int i = start; i < end; i++) {
+            // Any '%' found in the region starts an invalid format specifier.
+            if (s.charAt(i) == '%') {
+                char c = (i == end - 1) ? '%' : s.charAt(i + 1);
+                throw new UnknownFormatConversionException(String.valueOf(c));
             }
+        }
+    }
 
-            if (f.contains(Flags.GROUP)) {
-                if (l == null || l.equals(Locale.US)) {
-                    grpSep = ',';
-                    grpSize = 3;
-                } else {
-                    DecimalFormatSymbols dfs = DecimalFormatSymbols.getInstance(l);
-                    grpSep = dfs.getGroupingSeparator();
-                    DecimalFormat df = null;
-                    NumberFormat nf = NumberFormat.getNumberInstance(l);
-                    if (nf instanceof DecimalFormat) {
-                        df = (DecimalFormat) nf;
-                    } else {
+    /**
+     * Interface for Formatter specifiers.
+     */
+    private interface FormatToken {
 
-                        // Use DecimalFormat constructor to obtain the instance,
-                        // in case NumberFormat.getNumberInstance(l)
-                        // returns instance other than DecimalFormat
-                        LocaleProviderAdapter adapter = LocaleProviderAdapter
-                                .getAdapter(NumberFormatProvider.class, l);
-                        if (!(adapter instanceof ResourceBundleBasedAdapter)) {
-                            adapter = LocaleProviderAdapter.getResourceBundleBased();
-                        }
-                        String[] all = adapter.getLocaleResources(l)
-                                .getNumberPatterns();
-                        df = new DecimalFormat(all[0], dfs);
-                    }
-                    grpSize = df.getGroupingSize();
-                    // Some locales do not use grouping (the number
-                    // pattern for these locales does not contain group, e.g.
-                    // ("#0.###")), but specify a grouping separator.
-                    // To avoid unnecessary identification of the position of
-                    // grouping separator, reset its value with null character
-                    if (!df.isGroupingUsed() || grpSize == 0) {
-                        grpSep = '\0';
-                    }
-                }
+        /**
+         * Return the specifier index.
+         * @return the index
+         */
+        int index();
+
+    }
+
+    private static class FixedString implements FormatToken {
+        private String s;
+        private int start;
+        private int end;
+
+        FixedString(String s, int start, int end) {
+            this.s = s;
+            this.start = start;
+            this.end = end;
+        }
+
+        public int index() {
+            return -2;
+        }
+
+        public Formatter print(Formatter formatter) throws IOException {
+            formatter.out().append(s, start, end);
+            return formatter;
+        }
+
+        public String toString() {
+            return s.substring(start, end);
+        }
+    }
+
+    private static class FormatSpecifier implements FormatToken {
+        private int index = -1;
+        private int f = Flags.NONE;
+        private int width;
+        private int precision;
+        private Conversion conversion;
+        private DateTime dateTime;
+
+        FormatSpecifier(String s, Matcher m) {
+            index(s, m.start(1), m.end(1));
+            flags(s, m.start(2), m.end(2));
+            width(s, m.start(3), m.end(3));
+            precision(s, m.start(4), m.end(4));
+
+            int tTStart = m.start(5);
+            if (tTStart >= 0) {
+                conversion(s.charAt(tTStart));
+                dateTime = DateTime.lookup(s.charAt(m.start(6)));
+            } else {
+                conversion(s.charAt(m.start(6)));
             }
+            checkConversion();
+        }
 
-            // localize the digits inserting group separators as necessary
-            for (int j = offset; j < len; j++) {
-                if (j == dot) {
-                    sb.append(decSep);
-                    // no more group separators after the decimal separator
-                    grpSep = '\0';
-                    continue;
+        private void index(String s, int start, int end) {
+            if (start >= 0) {
+                try {
+                    // skip the trailing '$'
+                    index = Integer.parseInt(s, start, end - 1, 10);
+                } catch (NumberFormatException x) {
+                    assert (false);
                 }
+            } else {
+                index = 0;
+            }
+        }
+
+        private void flags(String s, int start, int end) {
+            f = Flags.parse(s, start, end);
+            if (Flags.contains(f, Flags.PREVIOUS))
+                index = -1;
+        }
 
-                char c = value.charAt(j);
-                sb.append((char) ((c - '0') + zero));
-                if (grpSep != '\0' && j != dot - 1 && ((dot - j) % grpSize == 1)) {
-                    sb.append(grpSep);
+        private void width(String s, int start, int end) {
+            width = -1;
+            if (start >= 0) {
+                try {
+                    width = Integer.parseInt(s, start, end, 10);
+                    if (width < 0)
+                        throw new IllegalFormatWidthException(width);
+                } catch (NumberFormatException x) {
+                    assert (false);
                 }
             }
+        }
 
-            // apply zero padding
-            if (width != -1 && f.contains(Flags.ZERO_PAD)) {
-                for (int k = sb.length(); k < width; k++) {
-                    sb.insert(begin, zero);
+        private void precision(String s, int start, int end) {
+            precision = -1;
+            if (start >= 0) {
+                try {
+                    // skip the leading '.'
+                    precision = Integer.parseInt(s, start + 1, end, 10);
+                    if (precision < 0)
+                        throw new IllegalFormatPrecisionException(precision);
+                } catch (NumberFormatException x) {
+                    assert (false);
                 }
             }
+        }
+
+        private void conversion(char conv) {
+            conversion = Conversion.lookup(conv);
+            if (Character.isUpperCase(conv)) {
+                f = Flags.add(f, Flags.UPPERCASE);
+                conversion = Conversion.lookup(Character.toLowerCase(conv));
+            }
+            if (Conversion.isText(conversion)) {
+                index = -2;
+            }
+        }
+
+        public int index() {
+            return index;
+        }
+
+        public String value() {
+            return toString();
+        }
+
+        public Conversion conversion() {
+            return conversion;
+        }
+
+        public DateTime dateTime() {
+            return dateTime;
+        }
+
+        public int flags() {
+            return f;
+        }
+
+        public int width() {
+            return width;
+        }
 
-            return sb;
+        public int precision() {
+            return precision;
         }
 
-        // Specialized localization of exponents, where the source value can only
-        // contain characters '0' through '9', starting at index offset, and no
-        // group separators is added for any locale.
-        private void localizedMagnitudeExp(StringBuilder sb, char[] value,
-                final int offset, Locale l) {
-            char zero = getZero(l);
+        public String toString() {
+            StringBuilder sb = new StringBuilder("%");
+            // Flags.UPPERCASE is set internally for legal conversions.
+            sb.append(Flags.toString(Flags.remove(f, Flags.UPPERCASE)));
+            if (index > 0)
+                sb.append(index).append('$');
+            if (width != -1)
+                sb.append(width);
+            if (precision != -1)
+                sb.append('.').append(precision);
+            sb.append(Flags.contains(f, Flags.UPPERCASE)
+                    ? Character.toUpperCase(conversion.c) : conversion.c);
+            if (dateTime != null)
+                sb.append(dateTime.c);
+            return sb.toString();
+        }
+
+        private void checkConversion() {
+            switch (conversion) {
+
+                // Conversions applicable to all objects.
+                case BOOLEAN:
+                case BOOLEAN_UPPER:
+                case STRING:
+                case STRING_UPPER:
+                case HASHCODE:
+                case HASHCODE_UPPER:
+                    checkGeneral();
+                    break;
+
+                // Conversions applicable to date objects.
+                case DATE_TIME:
+                case DATE_TIME_UPPER:
+                    checkDateTime();
+                    break;
+
+                // Conversions applicable to character.
+                case CHARACTER:
+                case CHARACTER_UPPER:
+                    checkCharacter();
+                    break;
+
+                // Conversions applicable to integer types.
+                case DECIMAL_INTEGER:
+                case OCTAL_INTEGER:
+                case HEXADECIMAL_INTEGER:
+                case HEXADECIMAL_INTEGER_UPPER:
+                    checkInteger();
+                    break;
+
+                // Conversions applicable to floating-point types.
+                case SCIENTIFIC:
+                case SCIENTIFIC_UPPER:
+                case GENERAL:
+                case GENERAL_UPPER:
+                case DECIMAL_FLOAT:
+                case HEXADECIMAL_FLOAT:
+                case HEXADECIMAL_FLOAT_UPPER:
+                    checkFloat();
+                    break;
+
+                // Conversions that do not require an argument
+                case LINE_SEPARATOR:
+                case PERCENT_SIGN:
+                    checkText();
+                    break;
 
-            int len = value.length;
-            for (int j = offset; j < len; j++) {
-                char c = value[j];
-                sb.append((char) ((c - '0') + zero));
             }
         }
-    }
 
-    private static class Flags {
-        private int flags;
+        private void checkGeneral() {
+            if ((conversion == Conversion.BOOLEAN || conversion == Conversion.HASHCODE)
+                    && Flags.contains(f, Flags.ALTERNATE))
+                failMismatch(Flags.ALTERNATE, conversion);
+            // '-' requires a width
+            if (width == -1 && Flags.contains(f, Flags.LEFT_JUSTIFY))
+                throw new MissingFormatWidthException(toString());
+            checkBadFlags(Flags.PLUS, Flags.LEADING_SPACE, Flags.ZERO_PAD,
+                    Flags.GROUP, Flags.PARENTHESES);
+        }
 
-        static final Flags NONE          = new Flags(0);      // ''
+        private void checkDateTime() {
+            if (precision != -1)
+                throw new IllegalFormatPrecisionException(precision);
+            if (dateTime == null)
+                throw new UnknownFormatConversionException(String.valueOf(conversion.c));
+            checkBadFlags(Flags.ALTERNATE, Flags.PLUS, Flags.LEADING_SPACE,
+                    Flags.ZERO_PAD, Flags.GROUP, Flags.PARENTHESES);
+            // '-' requires a width
+            if (width == -1 && Flags.contains(f, Flags.LEFT_JUSTIFY))
+                throw new MissingFormatWidthException(toString());
+        }
 
-        // duplicate declarations from Formattable.java
-        static final Flags LEFT_JUSTIFY  = new Flags(1<<0);   // '-'
-        static final Flags UPPERCASE     = new Flags(1<<1);   // '^'
-        static final Flags ALTERNATE     = new Flags(1<<2);   // '#'
+        private void checkCharacter() {
+            if (precision != -1)
+                throw new IllegalFormatPrecisionException(precision);
+            checkBadFlags(Flags.ALTERNATE, Flags.PLUS, Flags.LEADING_SPACE,
+                    Flags.ZERO_PAD, Flags.GROUP, Flags.PARENTHESES);
+            // '-' requires a width
+            if (width == -1 && Flags.contains(f, Flags.LEFT_JUSTIFY))
+                throw new MissingFormatWidthException(toString());
+        }
 
-        // numerics
-        static final Flags PLUS          = new Flags(1<<3);   // '+'
-        static final Flags LEADING_SPACE = new Flags(1<<4);   // ' '
-        static final Flags ZERO_PAD      = new Flags(1<<5);   // '0'
-        static final Flags GROUP         = new Flags(1<<6);   // ','
-        static final Flags PARENTHESES   = new Flags(1<<7);   // '('
+        private void checkInteger() {
+            checkNumeric();
+            if (precision != -1)
+                throw new IllegalFormatPrecisionException(precision);
 
-        // indexing
-        static final Flags PREVIOUS      = new Flags(1<<8);   // '<'
+            if (conversion == Conversion.DECIMAL_INTEGER)
+                checkBadFlags(Flags.ALTERNATE);
+            else if (conversion == Conversion.OCTAL_INTEGER)
+                checkBadFlags(Flags.GROUP);
+            else
+                checkBadFlags(Flags.GROUP);
+        }
 
-        private Flags(int f) {
-            flags = f;
+        public void checkBadFlags(int... badFlags) {
+            for (int badFlag : badFlags)
+                if (Flags.contains(f, badFlag))
+                    failMismatch(badFlag, conversion);
         }
 
-        public int valueOf() {
-            return flags;
+        private void checkFloat() {
+            checkNumeric();
+            if (conversion == Conversion.DECIMAL_FLOAT) {
+                // no check
+            } else if (conversion == Conversion.HEXADECIMAL_FLOAT) {
+                checkBadFlags(Flags.PARENTHESES, Flags.GROUP);
+            } else if (conversion == Conversion.SCIENTIFIC) {
+                checkBadFlags(Flags.GROUP);
+            } else if (conversion == Conversion.GENERAL) {
+                checkBadFlags(Flags.ALTERNATE);
+            }
         }
 
-        public boolean contains(Flags f) {
-            return (flags & f.valueOf()) == f.valueOf();
+        private void checkNumeric() {
+            if (width != -1 && width < 0)
+                throw new IllegalFormatWidthException(width);
+
+            if (precision != -1 && precision < 0)
+                throw new IllegalFormatPrecisionException(precision);
+
+            // '-' and '0' require a width
+            if (width == -1
+                    && (Flags.contains(f, Flags.LEFT_JUSTIFY) || Flags.contains(f, Flags.ZERO_PAD)))
+                throw new MissingFormatWidthException(toString());
+
+            // bad combination
+            if ((Flags.contains(f, Flags.PLUS) && Flags.contains(f, Flags.LEADING_SPACE))
+                    || (Flags.contains(f, Flags.LEFT_JUSTIFY) && Flags.contains(f, Flags.ZERO_PAD)))
+                throw new IllegalFormatFlagsException(Flags.toString(f));
         }
 
-        public Flags dup() {
-            return new Flags(flags);
+        private void checkText() {
+            if (precision != -1)
+                throw new IllegalFormatPrecisionException(precision);
+            switch (conversion) {
+                case PERCENT_SIGN:
+                    if (f != Flags.LEFT_JUSTIFY
+                            && f != Flags.NONE)
+                        throw new IllegalFormatFlagsException(Flags.toString(f));
+                    // '-' requires a width
+                    if (width == -1 && Flags.contains(f, Flags.LEFT_JUSTIFY))
+                        throw new MissingFormatWidthException(toString());
+                    break;
+                case LINE_SEPARATOR:
+                    if (width != -1)
+                        throw new IllegalFormatWidthException(width);
+                    if (f != Flags.NONE)
+                        throw new IllegalFormatFlagsException(Flags.toString(f));
+                    break;
+                default:
+                    assert false;
+            }
         }
 
-        private Flags add(Flags f) {
-            flags |= f.valueOf();
-            return this;
+        // -- Methods to support throwing exceptions --
+
+        public static void failMismatch(int flags, Conversion conersion) {
+            String fs = Flags.toString(flags);
+            throw new FormatFlagsConversionMismatchException(fs, conersion.c);
         }
 
-        public Flags remove(Flags f) {
-            flags &= ~f.valueOf();
-            return this;
+    }
+
+    private static class Flags {
+        public static final int NONE          = 0;         // ''
+
+        // duplicate declarations from Formattable.java
+        public static final int LEFT_JUSTIFY  = 1 << 0;   // '-'
+        public static final int UPPERCASE     = 1 << 1;   // '^'
+        public static final int ALTERNATE     = 1 << 2;   // '#'
+
+        // numerics
+        public static final int PLUS          = 1 << 3;   // '+'
+        public static final int LEADING_SPACE = 1 << 4;   // ' '
+        public static final int ZERO_PAD      = 1 << 5;   // '0'
+        public static final int GROUP         = 1 << 6;   // ','
+        public static final int PARENTHESES   = 1 << 7;   // '('
+
+        // indexing
+        public static final int PREVIOUS      = 1 << 8;   // '<'
+
+        private Flags() {}
+
+        public static boolean contains(int flags, int f) {
+            return (flags & f) == f;
+        }
+
+        public static int remove(int flags, int f) {
+            return flags & ~f;
         }
 
-        public static Flags parse(String s, int start, int end) {
-            Flags f = new Flags(0);
+        private static int add(int flags, int f) {
+            return flags | f;
+        }
+
+        private static int parse(String s, int start, int end) {
+            int f = NONE;
             for (int i = start; i < end; i++) {
                 char c = s.charAt(i);
-                Flags v = parse(c);
-                if (f.contains(v))
-                    throw new DuplicateFormatFlagsException(v.toString());
-                f.add(v);
+                int v = parse(c);
+                if (contains(f, v))
+                    throw new DuplicateFormatFlagsException(toString(v));
+                f = add(f, v);
             }
             return f;
         }
 
         // parse those flags which may be provided by users
-        private static Flags parse(char c) {
+        private static int parse(char c) {
             switch (c) {
-            case '-': return LEFT_JUSTIFY;
-            case '#': return ALTERNATE;
-            case '+': return PLUS;
-            case ' ': return LEADING_SPACE;
-            case '0': return ZERO_PAD;
-            case ',': return GROUP;
-            case '(': return PARENTHESES;
-            case '<': return PREVIOUS;
-            default:
-                throw new UnknownFormatFlagsException(String.valueOf(c));
+                case '-': return LEFT_JUSTIFY;
+                case '#': return ALTERNATE;
+                case '+': return PLUS;
+                case ' ': return LEADING_SPACE;
+                case '0': return ZERO_PAD;
+                case ',': return GROUP;
+                case '(': return PARENTHESES;
+                case '<': return PREVIOUS;
+                default:
+                    throw new UnknownFormatFlagsException(String.valueOf(c));
             }
         }
 
-        // Returns a string representation of the current {@code Flags}.
-        public static String toString(Flags f) {
-            return f.toString();
-        }
-
-        public String toString() {
+        // Returns a string representation of the current {@code flags}.
+        public static String toString(int f) {
             StringBuilder sb = new StringBuilder();
-            if (contains(LEFT_JUSTIFY))  sb.append('-');
-            if (contains(UPPERCASE))     sb.append('^');
-            if (contains(ALTERNATE))     sb.append('#');
-            if (contains(PLUS))          sb.append('+');
-            if (contains(LEADING_SPACE)) sb.append(' ');
-            if (contains(ZERO_PAD))      sb.append('0');
-            if (contains(GROUP))         sb.append(',');
-            if (contains(PARENTHESES))   sb.append('(');
-            if (contains(PREVIOUS))      sb.append('<');
+            if (contains(f, LEFT_JUSTIFY))  sb.append('-');
+            if (contains(f, UPPERCASE))     sb.append('^');
+            if (contains(f, ALTERNATE))     sb.append('#');
+            if (contains(f, PLUS))          sb.append('+');
+            if (contains(f, LEADING_SPACE)) sb.append(' ');
+            if (contains(f, ZERO_PAD))      sb.append('0');
+            if (contains(f, GROUP))         sb.append(',');
+            if (contains(f, PARENTHESES))   sb.append('(');
+            if (contains(f, PREVIOUS))      sb.append('<');
             return sb.toString();
         }
     }
 
-    private static class Conversion {
+    private enum Conversion {
+        NONE('\0'),
         // Byte, Short, Integer, Long, BigInteger
         // (and associated primitives due to autoboxing)
-        static final char DECIMAL_INTEGER     = 'd';
-        static final char OCTAL_INTEGER       = 'o';
-        static final char HEXADECIMAL_INTEGER = 'x';
-        static final char HEXADECIMAL_INTEGER_UPPER = 'X';
+        DECIMAL_INTEGER('d'),            // 'd'
+        OCTAL_INTEGER('o'),              // 'o'
+        HEXADECIMAL_INTEGER('x'),        // 'x'
+        HEXADECIMAL_INTEGER_UPPER('X'),  // 'X'
 
         // Float, Double, BigDecimal
         // (and associated primitives due to autoboxing)
-        static final char SCIENTIFIC          = 'e';
-        static final char SCIENTIFIC_UPPER    = 'E';
-        static final char GENERAL             = 'g';
-        static final char GENERAL_UPPER       = 'G';
-        static final char DECIMAL_FLOAT       = 'f';
-        static final char HEXADECIMAL_FLOAT   = 'a';
-        static final char HEXADECIMAL_FLOAT_UPPER = 'A';
+        SCIENTIFIC('e'),                 // 'e';
+        SCIENTIFIC_UPPER('E'),           // 'E';
+        GENERAL('g'),                    // 'g';
+        GENERAL_UPPER('G'),              // 'G';
+        DECIMAL_FLOAT('f'),              // 'f';
+        HEXADECIMAL_FLOAT('a'),          // 'a';
+        HEXADECIMAL_FLOAT_UPPER('A'),   // 'A';
 
         // Character, Byte, Short, Integer
         // (and associated primitives due to autoboxing)
-        static final char CHARACTER           = 'c';
-        static final char CHARACTER_UPPER     = 'C';
+        CHARACTER('c'),                 // 'c';
+        CHARACTER_UPPER('C'),           // 'C';
 
         // java.util.Date, java.util.Calendar, long
-        static final char DATE_TIME           = 't';
-        static final char DATE_TIME_UPPER     = 'T';
+        DATE_TIME('t'),                 // 't';
+        DATE_TIME_UPPER('T'),           // 'T';
 
         // if (arg.TYPE != boolean) return boolean
         // if (arg != null) return true; else return false;
-        static final char BOOLEAN             = 'b';
-        static final char BOOLEAN_UPPER       = 'B';
+        BOOLEAN('b'),                   // 'b';
+        BOOLEAN_UPPER('B'),             // 'B';
         // if (arg instanceof Formattable) arg.formatTo()
         // else arg.toString();
-        static final char STRING              = 's';
-        static final char STRING_UPPER        = 'S';
+        STRING('s'),                    // 's';
+        STRING_UPPER('S'),              // 'S';
         // arg.hashCode()
-        static final char HASHCODE            = 'h';
-        static final char HASHCODE_UPPER      = 'H';
+        HASHCODE('h'),                  // 'h';
+        HASHCODE_UPPER('H'),            // 'H';
 
-        static final char LINE_SEPARATOR      = 'n';
-        static final char PERCENT_SIGN        = '%';
+        LINE_SEPARATOR('n'),            // 'n';
+        PERCENT_SIGN('%');              // '%';
 
-        static boolean isValid(char c) {
-            return (isGeneral(c) || isInteger(c) || isFloat(c) || isText(c)
-                    || c == 't' || isCharacter(c));
+        private final char c;
+
+        Conversion (char c) {
+            this.c = c;
         }
 
-        // Returns true iff the Conversion is applicable to all objects.
-        static boolean isGeneral(char c) {
+        static Conversion lookup(char c) {
             switch (c) {
-            case BOOLEAN:
-            case BOOLEAN_UPPER:
-            case STRING:
-            case STRING_UPPER:
-            case HASHCODE:
-            case HASHCODE_UPPER:
-                return true;
-            default:
-                return false;
+                case 'd': return DECIMAL_INTEGER;
+                case 'o': return OCTAL_INTEGER;
+                case 'x': return HEXADECIMAL_INTEGER;
+                case 'X': return HEXADECIMAL_INTEGER_UPPER;
+                case 'e': return SCIENTIFIC;
+                case 'E': return SCIENTIFIC_UPPER;
+                case 'g': return GENERAL;
+                case 'G': return GENERAL_UPPER;
+                case 'f': return DECIMAL_FLOAT;
+                case 'a': return HEXADECIMAL_FLOAT;
+                case 'A': return HEXADECIMAL_FLOAT_UPPER;
+                case 'c': return CHARACTER;
+                case 'C': return CHARACTER_UPPER;
+                case 't': return DATE_TIME;
+                case 'T': return DATE_TIME_UPPER;
+                case 'b': return BOOLEAN;
+                case 'B': return BOOLEAN_UPPER;
+                case 's': return STRING;
+                case 'S': return STRING_UPPER;
+                case 'h': return HASHCODE;
+                case 'H': return HASHCODE_UPPER;
+                case 'n': return LINE_SEPARATOR;
+                case '%': return PERCENT_SIGN;
+                default:
+                    throw new UnknownFormatConversionException(String.valueOf(c));
+            }
+        }
+
+        public void fail(Object arg) {
+            throw new IllegalFormatConversionException(c, arg.getClass());
+        }
+
+        static boolean isText(Conversion conv) {
+            switch (conv) {
+                case LINE_SEPARATOR:
+                case PERCENT_SIGN:
+                    return true;
+                default:
+                    return false;
             }
         }
+    }
 
-        // Returns true iff the Conversion is applicable to character.
-        static boolean isCharacter(char c) {
+    private enum DateTime {
+        HOUR_OF_DAY_0('H'),            // 'H' (00 - 23)
+        HOUR_0('I'),                   // 'I' (01 - 12)
+        HOUR_OF_DAY('k'),              // 'k'  (0 - 23) -- like H
+        HOUR('l'),                     // 'l'  (1 - 12) -- like I
+        MINUTE('M'),                   // 'M'  (00 - 59)
+        NANOSECOND('N'),               // 'N'  (000000000 - 999999999)
+        MILLISECOND('L'),              // 'L'  jdk, not in gnu (000 - 999)
+        MILLISECOND_SINCE_EPOCH('Q'), // 'Q'  (0 - 99...?)
+        AM_PM('p'),                    // 'p'  (am or pm)
+        SECONDS_SINCE_EPOCH('s'),     // 's'  (0 - 99...?)
+        SECOND('S'),                   // 'S'  (00 - 60 - leap second)
+        TIME('T'),                     // 'T'  (24 hour hh:mm:ss)
+        ZONE_NUMERIC('z'),             // 'z'  (-1200 - +1200) - ls minus?
+        ZONE('Z'),                     // 'Z'  (symbol)
+
+        // Date
+        NAME_OF_DAY_ABBREV('a'),       // 'a' 'a'
+        NAME_OF_DAY('A'),              // 'A' 'A'
+        NAME_OF_MONTH_ABBREV('b'),    // 'b' 'b'
+        NAME_OF_MONTH('B'),            // 'B'  'B'
+        CENTURY('C'),                  // 'C' (00 - 99)
+        DAY_OF_MONTH_0('d'),           // 'd' (01 - 31)
+        DAY_OF_MONTH('e'),             // 'e' (1 - 31) -- like d
+        // *    ISO_WEEK_OF_YEAR_2('g'),       // 'g'  cross %y %V
+// *    ISO_WEEK_OF_YEAR_4('G'),       // 'G'  cross %Y %V
+        NAME_OF_MONTH_ABBREV_X('h'),  // 'h'  -- same b
+        DAY_OF_YEAR('j'),              // 'j'  (001 - 366)
+        MONTH('m'),                    // 'm'  (01 - 12)
+        // *    DAY_OF_WEEK_1('u'),             // 'u'  (1 - 7) Monday
+// *    WEEK_OF_YEAR_SUNDAY('U'),       // 'U'  (0 - 53) Sunday+
+// *    WEEK_OF_YEAR_MONDAY_01('V'),    // 'V'  (01 - 53) Monday+
+// *    DAY_OF_WEEK_0('w'),             // 'w'  (0 - 6) Sunday
+// *    WEEK_OF_YEAR_MONDAY('W'),       // 'W'  (00 - 53) Monday
+        YEAR_2('y'),                    // 'y'  (00 - 99)
+        YEAR_4('Y'),                    // 'Y'  (0000 - 9999)
+
+        // Composites
+        TIME_12_HOUR('r'),             // 'r'  (hh:mm:ss [AP]M)
+        TIME_24_HOUR('R'),             // 'R'  (hh:mm same as %H:%M)
+        // *    LOCALE_TIME('X'),               // 'X'  (%H:%M:%S) - parse format?
+        DATE_TIME('c'),                // 'c'  (Sat Nov 04 12:02:33 EST 1999)
+        DATE('D'),                     // 'D'  (mm/dd/yy)
+        ISO_STANDARD_DATE('F');       // 'F'  (%Y-%m-%d)
+// *    LOCALE_DATE('x')                // 'x'  (mm/dd/yy)
+
+        static DateTime lookup(char c) {
             switch (c) {
-            case CHARACTER:
-            case CHARACTER_UPPER:
-                return true;
-            default:
+                case 'H': return HOUR_OF_DAY_0;
+                case 'I': return HOUR_0;
+                case 'k': return HOUR_OF_DAY;
+                case 'l': return HOUR;
+                case 'M': return MINUTE;
+                case 'N': return NANOSECOND;
+                case 'L': return MILLISECOND;
+                case 'Q': return MILLISECOND_SINCE_EPOCH;
+                case 'p': return AM_PM;
+                case 's': return SECONDS_SINCE_EPOCH;
+                case 'S': return SECOND;
+                case 'T': return TIME;
+                case 'z': return ZONE_NUMERIC;
+                case 'Z': return ZONE;
+
+                // Date
+                case 'a': return NAME_OF_DAY_ABBREV;
+                case 'A': return NAME_OF_DAY;
+                case 'b': return NAME_OF_MONTH_ABBREV;
+                case 'B': return NAME_OF_MONTH;
+                case 'C': return CENTURY;
+                case 'd': return DAY_OF_MONTH_0;
+                case 'e': return DAY_OF_MONTH;
+// *        case 'g': return ISO_WEEK_OF_YEAR_2;
+// *        case 'G': return ISO_WEEK_OF_YEAR_4;
+                case 'h': return NAME_OF_MONTH_ABBREV_X;
+                case 'j': return DAY_OF_YEAR;
+                case 'm': return MONTH;
+// *        case 'u': return DAY_OF_WEEK_1;
+// *        case 'U': return WEEK_OF_YEAR_SUNDAY;
+// *        case 'V': return WEEK_OF_YEAR_MONDAY_01;
+// *        case 'w': return DAY_OF_WEEK_0;
+// *        case 'W': return WEEK_OF_YEAR_MONDAY;
+                case 'y': return YEAR_2;
+                case 'Y': return YEAR_4;
+
+                // Composites
+                case 'r': return TIME_12_HOUR;
+                case 'R': return TIME_24_HOUR;
+// *        case 'X': return LOCALE_TIME;
+                case 'c': return DATE_TIME;
+                case 'D': return DATE;
+                case 'F': return ISO_STANDARD_DATE;
+// *        case 'x': return LOCALE_DATE;
+                default:
+                    throw new UnknownFormatConversionException("t" + c);
+            }
+        }
+
+        private final char c;
+
+        DateTime(char c) {
+            this.c = c;
+        }
+
+        public void fail(Object arg) {
+            throw new IllegalFormatConversionException(c, arg.getClass());
+        }
+    }
+
+    // Formatter BSM
+
+    private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
+
+    private static final MethodHandle SPECIFIER_PRINT =
+            findVirtualMethodHandle(Formatter.class, "print",
+                    methodType(Formatter.class, FormatSpecifier.class, Object.class, Locale.class));
+    private static final MethodHandle SPECIFIER_PRINT_STRING =
+            findVirtualMethodHandle(Formatter.class, "print",
+                    methodType(Formatter.class, FormatSpecifier.class, String.class, Locale.class));
+    private static final MethodHandle SPECIFIER_PRINT_INT =
+            findVirtualMethodHandle(Formatter.class, "print",
+                    methodType(Formatter.class, FormatSpecifier.class, int.class, Locale.class));
+    private static final MethodHandle SPECIFIER_PRINT_LONG =
+            findVirtualMethodHandle(Formatter.class, "print",
+                    methodType(Formatter.class, FormatSpecifier.class, long.class, Locale.class));
+    private static final MethodHandle SPECIFIER_PRINT_BYTE =
+            findVirtualMethodHandle(Formatter.class, "print",
+                    methodType(Formatter.class, FormatSpecifier.class, byte.class, Locale.class));
+    private static final MethodHandle SPECIFIER_PRINT_SHORT =
+            findVirtualMethodHandle(Formatter.class, "print",
+                    methodType(Formatter.class, FormatSpecifier.class, short.class, Locale.class));
+    private static final MethodHandle SPECIFIER_PRINT_FLOAT =
+            findVirtualMethodHandle(Formatter.class, "print",
+                    methodType(Formatter.class, FormatSpecifier.class, float.class, Locale.class));
+    private static final MethodHandle SPECIFIER_PRINT_DOUBLE =
+            findVirtualMethodHandle(Formatter.class, "print",
+                    methodType(Formatter.class, FormatSpecifier.class, double.class, Locale.class));
+    private static final MethodHandle SPECIFIER_PRINT_HASHCODE =
+            findVirtualMethodHandle(Formatter.class, "printHashCode",
+                    methodType(Formatter.class, FormatSpecifier.class, Object.class, Locale.class));
+    private static final MethodHandle FIXED_STRING_PRINT =
+            findVirtualMethodHandle(FixedString.class, "print",
+                    methodType(Formatter.class, Formatter.class));
+
+    private static final MethodHandle CONSTRUCT_FORMATTER =
+            findConstructorMethodHandle(Formatter.class, methodType(void.class))
+                    .asType(methodType(Formatter.class));
+    private static final MethodHandle CONSTRUCT_FORMATTER_APPENDABLE =
+            findConstructorMethodHandle(Formatter.class, methodType(void.class, Appendable.class))
+                    .asType(methodType(Formatter.class, Appendable.class));
+
+    private static final MethodHandle CONSTRUCT_MISSING_FORMAT_ARGUMENT_EXCEPTION =
+            findConstructorMethodHandle(MissingFormatArgumentException.class, methodType(void.class, String.class));
+    private static final MethodHandle APPENDABLE_TO_STRING =
+            findVirtualMethodHandle(Appendable.class, "toString", methodType(String.class));
+    private static final MethodHandle FORMATTER_OUT =
+            findVirtualMethodHandle(Formatter.class, "out", methodType(Appendable.class));
+    private static final MethodHandle LOCALE_GETDEFAULT =
+            insertArguments(findStaticMethodHandle(Locale.class, "getDefault",
+                    methodType(Locale.class, Locale.Category.class)),0, Locale.Category.FORMAT);
+    private static final MethodHandle FORMATTER_LOCALE =
+            findVirtualMethodHandle(Formatter.class, "locale", methodType(Locale.class));
+    private static final MethodHandle ILLEGAL_FORMAT_EXCEPTION_CLONE =
+            findVirtualMethodHandle(IllegalFormatException.class, "clone", methodType(IllegalFormatException.class));
+
+    private static final MethodHandle INT_TO_STRING =
+            findStaticMethodHandle(Integer.class, "toString", methodType(String.class, int.class));
+    private static final MethodHandle BOOLEAN_TO_STRING =
+            findStaticMethodHandle(Boolean.class, "toString", methodType(String.class, boolean.class));
+    private static final MethodHandle OBJECT_HASHCODE =
+            findVirtualMethodHandle(Object.class, "hashCode", methodType(int.class));
+    private static final MethodHandle INTEGER_TO_HEX_STRING =
+            findStaticMethodHandle(Integer.class, "toHexString", methodType(String.class, int.class));
+    private static final MethodHandle INTEGER_TO_OCTAL_STRING =
+            findStaticMethodHandle(Integer.class, "toOctalString", methodType(String.class, int.class));
+    private static final MethodHandle LONG_TO_STRING =
+            findStaticMethodHandle(Long.class, "toString", methodType(String.class, long.class));
+    private static final MethodHandle LONG_TO_HEX_STRING =
+            findStaticMethodHandle(Long.class, "toHexString", methodType(String.class, long.class));
+    private static final MethodHandle LONG_TO_OCTAL_STRING =
+            findStaticMethodHandle(Long.class, "toOctalString", methodType(String.class, long.class));
+    private static final MethodHandle STRING_TO_UPPER_CASE =
+            findVirtualMethodHandle(String.class, "toUpperCase", methodType(String.class));
+
+    private static final MethodHandle LOCALE_GUARD = findStaticMethodHandle(Formatter.class, "localeGuard",
+            methodType(boolean.class, Locale.class, Locale.class));
+    private static final MethodHandle BOOLEAN_OBJECT_FILTER = findStaticMethodHandle(Formatter.class, "booleanObjectFilter",
+            methodType(boolean.class, Object.class));
+    private static final MethodHandle NOT_NULL_TEST = findStaticMethodHandle(Formatter.class, "notNullTest",
+            methodType(boolean.class, Object.class));
+
+    private static final int MISSING_ARGUMENT_INDEX = Integer.MIN_VALUE;
+
+    /**
+     * formatterFormatBootstrap bootstrap.
+     * @param lookup               MethodHandles lookup
+     * @param name                 Name of method
+     * @param methodType           Method signature
+     * @param format               Formatter format string
+     * @param isStringMethod       Method's owner is String or not
+     * @param hasLocale            has Locale
+     * @throws NoSuchMethodException no such method
+     * @throws IllegalAccessException illegal access
+     * @throws StringConcatException string concat error
+     * @return Callsite for intrinsic method
+     */
+    public static CallSite formatterBootstrap(MethodHandles.Lookup lookup,
+                                              String name,
+                                              MethodType methodType,
+                                              String format,
+                                              int isStringMethod,
+                                              int hasLocale)
+            throws NoSuchMethodException, IllegalAccessException, StringConcatException {
+        boolean isString = isStringMethod == 1;
+        boolean hasLocaleArg = hasLocale == 1;
+        boolean isVarArgs = isVarArgsType(methodType, isString, hasLocaleArg);
+        if (isVarArgs) {
+            return new ConstantCallSite(fallbackMethodHandle(
+                    lookup, name, methodType, format,
+                    isString, hasLocaleArg, isVarArgs));
+        }
+
+        List<FormatToken> specs;
+
+        try {
+            specs = parse(format);
+        } catch (IllegalFormatException illegalFormatException) {
+            return new ConstantCallSite(illegalFormatThrower(illegalFormatException, methodType));
+        }
+
+        if (specs.isEmpty()) {
+            return new ConstantCallSite(isString ?
+                    constant(String.class, "").asType(methodType) :
+                    identity(methodType.parameterType(0)).asType(methodType));
+        }
+        // Array of formatter args excluding target and locale
+        Class<?>[] argTypes = methodType.dropParameterTypes(0, firstFormatterArg(isString, hasLocaleArg)).parameterArray();
+        // index array is needed for argument analysis
+        int[] argIndexes = calculateArgumentIndexes(specs, argTypes.length);
+        return isString && mayNotNeedFormatter(specs, argTypes, argIndexes) ?
+                new ConstantCallSite(new StringConcatHandleBuilder(specs, argTypes, argIndexes, hasLocaleArg).getHandle(lookup, methodType)) :
+                new ConstantCallSite(fallbackMethodHandle(lookup, name, methodType, format, isString, hasLocaleArg, isVarArgs));
+    }
+
+    private static int[] calculateArgumentIndexes(List<FormatToken> specs, int argCount) {
+        int[] argIndexes = new int[specs.size()];
+        int last = -1;
+        int lasto = -1;
+
+        // Calculate indices and throw exceptions for missing arguments
+        for (int i = 0; i < specs.size(); i++) {
+            FormatToken ft = specs.get(i);
+
+            int index = ft.index();
+            switch (index) {
+                case -2:  // fixed string, "%n", or "%%"
+                    argIndexes[i] = -1;
+                    break;
+                case -1:  // relative index
+                    argIndexes[i] = (last < 0 || last >= argCount) ? MISSING_ARGUMENT_INDEX : last;
+                    break;
+                case 0:  // ordinary index
+                    lasto++;
+                    last = lasto;
+                    argIndexes[i] = (last < 0 || last >= argCount) ? MISSING_ARGUMENT_INDEX : last;
+                    break;
+                default:  // explicit index
+                    last = index - 1;
+                    argIndexes[i] = (last < 0 || last >= argCount) ? MISSING_ARGUMENT_INDEX : last;
+                    break;
+            }
+        }
+
+        return argIndexes;
+    }
+
+    private static boolean mayNotNeedFormatter(List<FormatToken> specs, Class<?>[] argTypes, int[] argIndexes) {
+        for (int i = 0; i < specs.size(); i++) {
+            if (argIndexes[i] >= 0
+                    && !canUseDirectConcat((FormatSpecifier) specs.get(i), argTypes[argIndexes[i]])
+                    && !canUseSpecialConverter((FormatSpecifier) specs.get(i), argTypes[argIndexes[i]])) {
                 return false;
             }
         }
 
-        // Returns true iff the Conversion is an integer type.
-        static boolean isInteger(char c) {
-            switch (c) {
+        return true;
+    }
+
+    private static int firstFormatterArg(boolean isStringMethod, boolean hasLocaleArg) {
+        int index = isStringMethod ? 0 : 1;
+        return hasLocaleArg ? index + 1 : index;
+    }
+
+    private static MethodHandle findVirtualMethodHandle(Class<?> type, String name, MethodType methodType) {
+        try {
+            return LOOKUP.findVirtual(type, name, methodType);
+        } catch (NoSuchMethodException | IllegalAccessException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private static MethodHandle findStaticMethodHandle(Class<?> type, String name, MethodType methodType) {
+        try {
+            return LOOKUP.findStatic(type, name, methodType);
+        } catch (NoSuchMethodException | IllegalAccessException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private static MethodHandle findConstructorMethodHandle(Class<?> type, MethodType methodType) {
+        try {
+            return LOOKUP.findConstructor(type, methodType);
+        } catch (NoSuchMethodException | IllegalAccessException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+
+    static abstract class FormatHandleBuilder {
+        final List<FormatToken> specs;
+        final Class<?>[] argTypes;
+        final int[] argIndexes;
+        final boolean hasLocaleArg;
+
+        FormatHandleBuilder(List<FormatToken> specs, Class<?>[] argTypes, int[] argIndexes, boolean hasLocaleArg) {
+            this.specs = specs;
+            this.argTypes = argTypes;
+            this.argIndexes = argIndexes;
+            this.hasLocaleArg = hasLocaleArg;
+        }
+
+        void buildHandles() {
+            for (int i = 0; i < specs.size(); i++) {
+                if (argIndexes[i] == -1) {
+                    addConstantMethodHandle(argTypes, specs.get(i));
+                } else if (argIndexes[i] == MISSING_ARGUMENT_INDEX) {
+                    addMissingArgumentMethodHandle(argTypes, (FormatSpecifier) specs.get(i));
+                } else {
+                    addArgumentMethodHandle(argTypes, (FormatSpecifier) specs.get(i), argIndexes[i]);
+                }
+            }
+        }
+
+        abstract void addArgumentMethodHandle(Class<?>[] argTypes, FormatSpecifier spec, int argIndex);
+        abstract void addConstantMethodHandle(Class<?>[] argTypes, FormatToken spec);
+        abstract void addMissingArgumentMethodHandle(Class<?>[] argTypes, FormatSpecifier spec);
+        abstract MethodHandle getHandle(MethodHandles.Lookup lookup, MethodType methodType);
+    }
+
+
+    static class FormatterFormatHandleBuilder extends FormatHandleBuilder {
+
+        private MethodHandle handle = null;
+        final boolean isFormatterMethod;
+        final boolean isStringMethod;
+
+        FormatterFormatHandleBuilder(List<FormatToken> specs, Class<?>[] argTypes, int[] argIndexes,
+                                     boolean hasLocaleArg, boolean isFormatterMethod, boolean isStringMethod) {
+            super(specs, argTypes, argIndexes, hasLocaleArg);
+            this.isFormatterMethod = isFormatterMethod;
+            this.isStringMethod = isStringMethod;
+        }
+
+        @Override
+        public void addArgumentMethodHandle(Class<?>[] argTypes, FormatSpecifier spec, int argIndex) {
+            MethodHandle appender;
+
+            if (canUseSpecialConverter(spec, argTypes[argIndex])) {
+                MethodHandle conversionFilter = getSpecializedConverter(spec, argTypes[argIndex]);
+                appender = filterArguments(SPECIFIER_PRINT_STRING, 2, conversionFilter);
+                appender = insertArguments(appender, 1, spec);
+            } else {
+                appender = getPrintHandle(argTypes[argIndex], spec);
+                appender = insertArguments(appender, 1, spec);
+            }
+
+            appender = appender.asType(appender.type().changeParameterType(1, argTypes[argIndex]));
+
+            if (argIndex > 0) {
+                appender = dropArguments(appender, 1, Arrays.copyOfRange(argTypes, 0, argIndex));
+            }
+            if (argIndex < argTypes.length - 1) {
+                appender = dropArguments(appender, argIndex + 2, Arrays.copyOfRange(argTypes, argIndex + 1, argTypes.length));
+            }
+
+            if (handle == null) {
+                handle = appender;
+            } else {
+                handle = foldArguments(appender, handle.asType(handle.type().changeReturnType(void.class)));
+            }
+        }
+
+        @Override
+        public void addConstantMethodHandle(Class<?>[] argTypes, FormatToken spec) {
+            MethodHandle appender;
+            if (spec instanceof FixedString) {
+                appender = dropArguments(insertArguments(FIXED_STRING_PRINT, 0, spec), 1, Locale.class);
+            } else {
+                appender = insertArguments(SPECIFIER_PRINT, 1, spec);
+                appender = insertArguments(appender, 1, (Object) null);
+            }
+            appender = dropArguments(appender, 1, Arrays.copyOfRange(argTypes, 0, argTypes.length));
+
+            if (handle == null) {
+                handle = appender;
+            } else {
+                handle = foldArguments(appender, handle.asType(handle.type().changeReturnType(void.class)));
+            }
+        }
+
+        @Override
+        public void addMissingArgumentMethodHandle(Class<?>[] argTypes, FormatSpecifier spec) {
+            MethodHandle thrower = missingFormatArgumentThrower(spec.toString(), Formatter.class);
+            thrower = dropArguments(thrower, 0, Formatter.class);
+            thrower = dropArguments(thrower, 1, argTypes);
+            thrower = dropArguments(thrower, thrower.type().parameterCount(), Locale.class);
+
+            if (handle == null) {
+                handle = thrower;
+            } else {
+                handle = foldArguments(thrower, handle.asType(handle.type().changeReturnType(void.class)));
+            }
+        }
+
+        @Override
+        public MethodHandle getHandle(MethodHandles.Lookup lookup, MethodType methodType) {
+
+            buildHandles();
+
+            MethodHandle wrapper;
+
+            if (isFormatterMethod) {
+                wrapper = handle;
+            } else {
+                if (isStringMethod) {
+                    wrapper = foldArguments(handle, 0, CONSTRUCT_FORMATTER);
+                } else {
+                    wrapper = filterArguments(handle, 0, CONSTRUCT_FORMATTER_APPENDABLE);
+                }
+                wrapper = filterReturnValue(wrapper, FORMATTER_OUT);
+            }
+
+            if (hasLocaleArg) {
+                int[] argmap = new int[methodType.parameterCount()];
+                if (!isStringMethod) {
+                    argmap[0] = 0;
+                    argmap[argmap.length - 1] = 1;
+                    for (int i = 1; i < argmap.length - 1; i++) {
+                        argmap[i] = i + 1;
+                    }
+                } else {
+                    argmap[argmap.length - 1] = 0;
+                    for (int i = 0; i < argmap.length - 1; i++) {
+                        argmap[i] = i + 1;
+                    }
+                }
+                MethodType newType = methodType.changeReturnType(wrapper.type().returnType());
+                if (!isStringMethod) {
+                    newType = newType.changeParameterType(0, wrapper.type().parameterType(0));
+                }
+                wrapper = MethodHandles.permuteArguments(wrapper, newType, argmap);
+            } else {
+                if (isFormatterMethod) {
+                    wrapper = foldLocaleFromFormatter(wrapper, methodType.parameterCount());
+                } else {
+                    wrapper = foldArguments(wrapper, methodType.parameterCount(), LOCALE_GETDEFAULT);
+                }
+            }
+            if (isStringMethod) {
+                wrapper = filterReturnValue(wrapper, APPENDABLE_TO_STRING);
+            }
+            return wrapper.asType(methodType);
+        }
+    }
+
+    static class StringConcatHandleBuilder extends FormatHandleBuilder {
+
+        MethodType concatType = methodType(String.class);
+        StringBuilder recipe = new StringBuilder();
+
+        List<Object> constants = new ArrayList<>();
+        List<Integer> reorder = new ArrayList<>();
+        List<MethodHandle> argumentFormatters = new ArrayList<>();
+
+        boolean needsLocaleGuard = false;
+
+        StringConcatHandleBuilder(List<FormatToken> specs, Class<?>[] argTypes, int[] argIndexes, boolean hasLocaleArg) {
+            super(specs, argTypes, argIndexes, hasLocaleArg);
+        }
+
+        @Override
+        public void addArgumentMethodHandle(Class<?>[] argTypes, FormatSpecifier spec, int argIndex) {
+
+            // Add argument token to recipe
+            recipe.append('\1');
+
+            Class<?> argType = argTypes[argIndex];
+            boolean useDirectConcat = canUseDirectConcat(spec, argType);
+            concatType = concatType.appendParameterTypes(useDirectConcat ? argType : String.class);
+            reorder.add(argIndex);
+
+            if (useDirectConcat) {
+                if (spec.conversion() == Conversion.DECIMAL_INTEGER) {
+                    // Direct string concat, but we need to guard against locales requiring Unicode decimal symbols
+                    needsLocaleGuard = true;
+                }
+                argumentFormatters.add(null);
+            } else {
+                // Direct handle requiring no formatter or localization
+                assert canUseSpecialConverter(spec, argType);
+                MethodHandle conversionFilter = getSpecializedConverter(spec, argType);
+                argumentFormatters.add(conversionFilter);
+
+            }
+        }
+
+        @Override
+        public void addConstantMethodHandle(Class<?>[] argTypes, FormatToken spec) {
+            String value = getConstantSpecValue(spec);
+            // '\1' and '\2' denote argument or constant to StringConcatFactory
+            if (value.indexOf('\1') == -1 && value.indexOf('\2') == -1) {
+                recipe.append(value);
+            } else {
+                recipe.append('\2');
+                constants.add(value);
+            }
+        }
+
+        @Override
+        public void addMissingArgumentMethodHandle(Class<?>[] argTypes, FormatSpecifier spec) {
+            MethodHandle thrower = throwException(void.class, MissingFormatArgumentException.class);
+            thrower = foldArguments(thrower, insertArguments(CONSTRUCT_MISSING_FORMAT_ARGUMENT_EXCEPTION, 0, spec.toString()));
+            argumentFormatters.add(thrower);
+        }
+
+        @Override
+        public MethodHandle getHandle(MethodHandles.Lookup lookup, MethodType methodType) {
+
+            buildHandles();
+
+            CallSite cs;
+
+            try {
+                cs = StringConcatFactory.makeConcatWithConstants(lookup, "formatterBootstrap", concatType, recipe.toString(), constants.toArray());
+            } catch (StringConcatException sce) {
+                throw new RuntimeException(sce);
+            }
+
+            MethodHandle handle = dropArguments(cs.getTarget(), concatType.parameterCount(), Locale.class);
+
+            int paramIndex = 0;
+
+            for (MethodHandle formatter : argumentFormatters) {
+
+                if (formatter != null) {
+                    int paramCount = formatter.type().parameterCount();
+                    if (paramCount == 0) {
+                        handle = foldArguments(handle, 0, formatter);
+                    } else {
+                        assert paramCount == 1;
+                        handle = filterArguments(handle, paramIndex, formatter);
+                    }
+                }
+
+                paramIndex++;
+            }
+
+            // move Locale argument from last to first position
+            handle = moveArgToFront(handle, handle.type().parameterCount() - 1);
+
+            if (needsLocaleGuard) {
+                // We have a decimal int without formatter - this doesn't work for
+                // locales using unicode decimal symbols, so add a guard and fallback handle for that case
+                Locale safeDefaultLocale = getSafeDefaultLocale();
+                MethodType mt = hasLocaleArg ? methodType : methodType.insertParameterTypes(0, Locale.class);
+                handle = MethodHandles.guardWithTest(
+                        insertArguments(LOCALE_GUARD, 0, safeDefaultLocale),
+                        handle,
+                        new FormatterFormatHandleBuilder(specs, argTypes, argIndexes, true, false, true)
+                                .getHandle(lookup, mt));
+            }
+
+            if (!hasLocaleArg) {
+                handle = foldArguments(handle, 0, LOCALE_GETDEFAULT);
+            }
+
+            int[] reorderArray = hasLocaleArg ?
+                    // Leading Locale arg - add initial element to keep it in place and increase other values by 1
+                    IntStream.concat(IntStream.of(0), reorder.stream().mapToInt(i -> i + 1)).toArray() :
+                    reorder.stream().mapToInt(i -> i).toArray();
+
+            return MethodHandles.permuteArguments(handle, methodType, reorderArray);
+        }
+    }
+
+    private static Locale getSafeDefaultLocale() {
+        Locale defaultLocale = Locale.getDefault(Locale.Category.FORMAT);
+        if (defaultLocale == null || DecimalFormatSymbols.getInstance(defaultLocale).getZeroDigit() != '0') {
+            defaultLocale = Locale.US;
+        }
+        return defaultLocale;
+    }
+
+    private static MethodHandle moveArgToFront(MethodHandle handle, int argIndex) {
+        Class<?>[] paramTypes = handle.type().parameterArray();
+
+        MethodType methodType = methodType(handle.type().returnType(), paramTypes[argIndex]);
+        int[] reorder = new int[paramTypes.length];
+        reorder[argIndex] = 0;
+
+        for (int i = 0, j = 1; i < paramTypes.length; i++) {
+            if (i != argIndex) {
+                methodType = methodType.appendParameterTypes(paramTypes[i]);
+                reorder[i] = j++;
+            }
+        }
+        return permuteArguments(handle, methodType, reorder);
+    }
+
+    private static MethodHandle foldLocaleFromFormatter(MethodHandle handle, int localeArgIndex) {
+        return foldArguments(moveArgToFront(handle, localeArgIndex), FORMATTER_LOCALE);
+    }
+
+    private static String getConstantSpecValue(Object o) {
+        if (o instanceof FormatSpecifier) {
+            FormatSpecifier spec = (FormatSpecifier) o;
+            assert spec.index() == -2;
+            if (spec.conversion() == Conversion.LINE_SEPARATOR) {
+                return System.lineSeparator();
+            } else if (spec.conversion() == Conversion.PERCENT_SIGN) {
+                return String.format(spec.toString());
+            }
+        }
+        return o.toString();
+    }
+
+    private static MethodHandle missingFormatArgumentThrower(String message, Class<?> returnType) {
+        MethodHandle thrower = throwException(returnType, MissingFormatArgumentException.class);
+        return foldArguments(thrower, insertArguments(CONSTRUCT_MISSING_FORMAT_ARGUMENT_EXCEPTION, 0, message));
+    }
+
+    private static MethodHandle illegalFormatThrower(IllegalFormatException illegalFormat, MethodType methodType) {
+        MethodHandle thrower = throwException(methodType.returnType(), IllegalFormatException.class);
+        thrower = foldArguments(thrower, 0, insertArguments(ILLEGAL_FORMAT_EXCEPTION_CLONE, 0, illegalFormat));
+        return dropArguments(thrower, 0, methodType.parameterArray());
+    }
+
+    private static boolean localeGuard(Locale locale1, Locale locale2) {
+        return locale1 == locale2;
+    }
+
+    private static boolean booleanObjectFilter(Object arg) {
+        return arg != null && (! (arg instanceof Boolean) || ((Boolean) arg));
+    }
+
+    private static boolean notNullTest(Object arg) {
+        return arg != null;
+    }
+
+
+    private static MethodHandle getSpecializedConverter(FormatSpecifier spec, Class<?> argType) {
+        MethodHandle conversionFilter;
+
+        switch (spec.conversion()) {
+            case HASHCODE:
+                conversionFilter = filterArguments(INTEGER_TO_HEX_STRING, 0, OBJECT_HASHCODE);
+                break;
             case DECIMAL_INTEGER:
-            case OCTAL_INTEGER:
+                conversionFilter = argType == long.class ? LONG_TO_STRING : INT_TO_STRING;
+                break;
             case HEXADECIMAL_INTEGER:
-            case HEXADECIMAL_INTEGER_UPPER:
-                return true;
+                conversionFilter =  argType == long.class ? LONG_TO_HEX_STRING : INTEGER_TO_HEX_STRING;
+                break;
+            case OCTAL_INTEGER:
+                conversionFilter =  argType == long.class ? LONG_TO_OCTAL_STRING : INTEGER_TO_OCTAL_STRING;
+                break;
+            case BOOLEAN:
+                conversionFilter = BOOLEAN_TO_STRING;
+                break;
             default:
-                return false;
-            }
+                throw new IllegalStateException("Unexpected conversion: " + spec.conversion());
         }
 
-        // Returns true iff the Conversion is a floating-point type.
-        static boolean isFloat(char c) {
-            switch (c) {
-            case SCIENTIFIC:
-            case SCIENTIFIC_UPPER:
-            case GENERAL:
-            case GENERAL_UPPER:
-            case DECIMAL_FLOAT:
-            case HEXADECIMAL_FLOAT:
-            case HEXADECIMAL_FLOAT_UPPER:
-                return true;
-            default:
-                return false;
+        if (conversionFilter.type().parameterType(0) != argType) {
+            if (spec.conversion() == Conversion.BOOLEAN)
+                conversionFilter = filterArguments(conversionFilter, 0, BOOLEAN_OBJECT_FILTER);
+            else if (! argType.isPrimitive())
+                conversionFilter = guardWithTest(NOT_NULL_TEST,
+                        conversionFilter.asType(methodType(String.class, Object.class)),
+                        dropArguments(constant(String.class, "null"), 0, Object.class));
+            conversionFilter = conversionFilter.asType(conversionFilter.type().changeParameterType(0, argType));
+        }
+
+        if (spec.flags() == Flags.UPPERCASE) {
+            conversionFilter = filterArguments(STRING_TO_UPPER_CASE,0, conversionFilter);
+        }
+
+        return conversionFilter;
+    }
+
+    private static boolean canUseSpecialConverter(FormatSpecifier spec, Class<?> argType) {
+        return (spec.flags() == Flags.NONE || spec.flags() == Flags.UPPERCASE)
+                && spec.width() == -1
+                && !requiresLocalization(spec)
+                && isSafeArgumentType(spec.conversion(), argType);
+    }
+
+    private static boolean canUseDirectConcat(FormatSpecifier spec, Class<?> argType) {
+        if (spec.flags() == Flags.NONE
+                && spec.width() == -1
+                && isSafeArgumentType(spec.conversion(), argType)) {
+            switch (spec.conversion()) {
+                case STRING:
+                case BOOLEAN:
+                case CHARACTER:
+                case DECIMAL_INTEGER:
+                case LINE_SEPARATOR:
+                case PERCENT_SIGN:
+                    return true;
             }
         }
+        return false;
+    }
 
-        // Returns true iff the Conversion does not require an argument
-        static boolean isText(char c) {
-            switch (c) {
+    private static boolean isSafeArgumentType(Conversion conversion, Class<?> type) {
+        if (conversion == Conversion.BOOLEAN) {
+            return type == boolean.class || type == Boolean.class;
+        }
+        if (conversion == Conversion.CHARACTER) {
+            return type == char.class || type == Character.class;
+        }
+        if (conversion == Conversion.DECIMAL_INTEGER
+                || conversion == Conversion.HEXADECIMAL_INTEGER
+                || conversion == Conversion.OCTAL_INTEGER) {
+            return type == int.class || type == long.class || type == Integer.class;
+        }
+        if (conversion == Conversion.HASHCODE) {
+            return true;
+        }
+        // Limit to String to prevent us from doing toString() on a java.util.Formattable
+        return conversion == Conversion.STRING && type == String.class;
+    }
+
+    private static boolean requiresLocalization(FormatSpecifier spec) {
+        switch (spec.conversion()) {
+            case BOOLEAN:
+            case HEXADECIMAL_INTEGER:
+            case OCTAL_INTEGER:
+            case HASHCODE:
             case LINE_SEPARATOR:
             case PERCENT_SIGN:
-                return true;
-            default:
                 return false;
-            }
+            default:
+                return true;
         }
     }
 
-    private static class DateTime {
-        static final char HOUR_OF_DAY_0 = 'H'; // (00 - 23)
-        static final char HOUR_0        = 'I'; // (01 - 12)
-        static final char HOUR_OF_DAY   = 'k'; // (0 - 23) -- like H
-        static final char HOUR          = 'l'; // (1 - 12) -- like I
-        static final char MINUTE        = 'M'; // (00 - 59)
-        static final char NANOSECOND    = 'N'; // (000000000 - 999999999)
-        static final char MILLISECOND   = 'L'; // jdk, not in gnu (000 - 999)
-        static final char MILLISECOND_SINCE_EPOCH = 'Q'; // (0 - 99...?)
-        static final char AM_PM         = 'p'; // (am or pm)
-        static final char SECONDS_SINCE_EPOCH = 's'; // (0 - 99...?)
-        static final char SECOND        = 'S'; // (00 - 60 - leap second)
-        static final char TIME          = 'T'; // (24 hour hh:mm:ss)
-        static final char ZONE_NUMERIC  = 'z'; // (-1200 - +1200) - ls minus?
-        static final char ZONE          = 'Z'; // (symbol)
+    private static MethodHandle getPrintHandle(Class<?> argType, FormatSpecifier spec) {
+        if (spec.conversion() == Conversion.HASHCODE) {
+            return SPECIFIER_PRINT_HASHCODE;
+        } else if (spec.conversion() == Conversion.DECIMAL_INTEGER && argType == int.class) {
+            return SPECIFIER_PRINT_INT;
+        } else if (spec.conversion() == Conversion.DECIMAL_INTEGER && argType == long.class) {
+            return SPECIFIER_PRINT_LONG;
+        } else if (spec.conversion() == Conversion.DECIMAL_INTEGER && argType == byte.class) {
+            return SPECIFIER_PRINT_BYTE;
+        } else if (spec.conversion() == Conversion.DECIMAL_INTEGER && argType == short.class) {
+            return SPECIFIER_PRINT_SHORT;
+        } else if (spec.conversion() == Conversion.DECIMAL_FLOAT && argType == float.class) {
+            return SPECIFIER_PRINT_FLOAT;
+        } else if (spec.conversion() == Conversion.DECIMAL_FLOAT && argType == double.class) {
+            return SPECIFIER_PRINT_DOUBLE;
+        } else {
+            return SPECIFIER_PRINT;
+        }
+    }
 
-        // Date
-        static final char NAME_OF_DAY_ABBREV    = 'a'; // 'a'
-        static final char NAME_OF_DAY           = 'A'; // 'A'
-        static final char NAME_OF_MONTH_ABBREV  = 'b'; // 'b'
-        static final char NAME_OF_MONTH         = 'B'; // 'B'
-        static final char CENTURY               = 'C'; // (00 - 99)
-        static final char DAY_OF_MONTH_0        = 'd'; // (01 - 31)
-        static final char DAY_OF_MONTH          = 'e'; // (1 - 31) -- like d
-// *    static final char ISO_WEEK_OF_YEAR_2    = 'g'; // cross %y %V
-// *    static final char ISO_WEEK_OF_YEAR_4    = 'G'; // cross %Y %V
-        static final char NAME_OF_MONTH_ABBREV_X  = 'h'; // -- same b
-        static final char DAY_OF_YEAR           = 'j'; // (001 - 366)
-        static final char MONTH                 = 'm'; // (01 - 12)
-// *    static final char DAY_OF_WEEK_1         = 'u'; // (1 - 7) Monday
-// *    static final char WEEK_OF_YEAR_SUNDAY   = 'U'; // (0 - 53) Sunday+
-// *    static final char WEEK_OF_YEAR_MONDAY_01 = 'V'; // (01 - 53) Monday+
-// *    static final char DAY_OF_WEEK_0         = 'w'; // (0 - 6) Sunday
-// *    static final char WEEK_OF_YEAR_MONDAY   = 'W'; // (00 - 53) Monday
-        static final char YEAR_2                = 'y'; // (00 - 99)
-        static final char YEAR_4                = 'Y'; // (0000 - 9999)
+    private static MethodHandle fallbackMethodHandle(MethodHandles.Lookup lookup, String name,
+                                                     MethodType methodType, String format, boolean isStringMethod,
+                                                     boolean hasLocaleArg, boolean isVarArgs) {
+        if (isStringMethod) {
+            MethodHandle handle = findStaticMethodHandle(lookup, String.class, name,
+                    hasLocaleArg ? methodType(String.class, Locale.class, String.class, Object[].class)
+                            : methodType(String.class, String.class, Object[].class));
+            return wrapHandle(handle, hasLocaleArg ? 1 : 0, format, methodType, isVarArgs);
+        }
+        Class<?> type = methodType.parameterType(0);
+        MethodHandle handle = findVirtualMethodHandle(lookup, type, name,
+                hasLocaleArg ? methodType(type, Locale.class, String.class, Object[].class)
+                        : methodType(type, String.class, Object[].class));
+        return wrapHandle(handle, hasLocaleArg ? 2 : 1, format, methodType, isVarArgs);
+    }
 
-        // Composites
-        static final char TIME_12_HOUR  = 'r'; // (hh:mm:ss [AP]M)
-        static final char TIME_24_HOUR  = 'R'; // (hh:mm same as %H:%M)
-// *    static final char LOCALE_TIME   = 'X'; // (%H:%M:%S) - parse format?
-        static final char DATE_TIME             = 'c';
-                                            // (Sat Nov 04 12:02:33 EST 1999)
-        static final char DATE                  = 'D'; // (mm/dd/yy)
-        static final char ISO_STANDARD_DATE     = 'F'; // (%Y-%m-%d)
-// *    static final char LOCALE_DATE           = 'x'; // (mm/dd/yy)
+    private static MethodHandle wrapHandle(MethodHandle handle, int formatArgIndex, String format, MethodType methodType, boolean isVarArg) {
+        MethodHandle h = MethodHandles.insertArguments(handle, formatArgIndex, format);
+        if (!isVarArg) {
+            h = h.asCollector(Object[].class, methodType.parameterCount() - formatArgIndex);
+        }
+        return h.asType(methodType);
+    }
 
-        static boolean isValid(char c) {
-            switch (c) {
-            case HOUR_OF_DAY_0:
-            case HOUR_0:
-            case HOUR_OF_DAY:
-            case HOUR:
-            case MINUTE:
-            case NANOSECOND:
-            case MILLISECOND:
-            case MILLISECOND_SINCE_EPOCH:
-            case AM_PM:
-            case SECONDS_SINCE_EPOCH:
-            case SECOND:
-            case TIME:
-            case ZONE_NUMERIC:
-            case ZONE:
+    private static boolean isVarArgsType(MethodType methodType, boolean isStringMethod, boolean hasLocaleArg) {
+        int expectedArrayArgument = (isStringMethod ? 0 : 1) + (hasLocaleArg ? 1 : 0);
+        return methodType.parameterCount() == expectedArrayArgument + 1
+                && methodType.parameterType(expectedArrayArgument) == Object[].class;
+    }
 
-            // Date
-            case NAME_OF_DAY_ABBREV:
-            case NAME_OF_DAY:
-            case NAME_OF_MONTH_ABBREV:
-            case NAME_OF_MONTH:
-            case CENTURY:
-            case DAY_OF_MONTH_0:
-            case DAY_OF_MONTH:
-// *        case ISO_WEEK_OF_YEAR_2:
-// *        case ISO_WEEK_OF_YEAR_4:
-            case NAME_OF_MONTH_ABBREV_X:
-            case DAY_OF_YEAR:
-            case MONTH:
-// *        case DAY_OF_WEEK_1:
-// *        case WEEK_OF_YEAR_SUNDAY:
-// *        case WEEK_OF_YEAR_MONDAY_01:
-// *        case DAY_OF_WEEK_0:
-// *        case WEEK_OF_YEAR_MONDAY:
-            case YEAR_2:
-            case YEAR_4:
+    private static MethodHandle findVirtualMethodHandle(MethodHandles.Lookup lookup, Class<?> type, String name, MethodType methodType) {
+        try {
+            return lookup.findVirtual(type, name, methodType);
+        } catch (NoSuchMethodException | IllegalAccessException e) {
+            throw new RuntimeException(e);
+        }
+    }
 
-            // Composites
-            case TIME_12_HOUR:
-            case TIME_24_HOUR:
-// *        case LOCALE_TIME:
-            case DATE_TIME:
-            case DATE:
-            case ISO_STANDARD_DATE:
-// *        case LOCALE_DATE:
-                return true;
-            default:
-                return false;
-            }
+    private static MethodHandle findStaticMethodHandle(MethodHandles.Lookup lookup, Class<?> type, String name, MethodType methodType) {
+        try {
+            return lookup.findStatic(type, name, methodType);
+        } catch (NoSuchMethodException | IllegalAccessException e) {
+            throw new RuntimeException(e);
         }
     }
 }
< prev index next >