| /* Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package java.util; |
| |
| import java.io.BufferedWriter; |
| import java.io.Closeable; |
| import java.io.File; |
| import java.io.FileNotFoundException; |
| import java.io.FileOutputStream; |
| import java.io.Flushable; |
| import java.io.IOException; |
| import java.io.OutputStream; |
| import java.io.OutputStreamWriter; |
| import java.io.PrintStream; |
| import java.io.UnsupportedEncodingException; |
| import java.math.BigDecimal; |
| import java.math.BigInteger; |
| import java.math.MathContext; |
| import java.nio.charset.Charset; |
| import libcore.icu.LocaleData; |
| import libcore.icu.NativeDecimalFormat; |
| import libcore.io.IoUtils; |
| |
| /** |
| * Formats arguments according to a format string (like {@code printf} in C). |
| * <p> |
| * It's relatively rare to use a {@code Formatter} directly. A variety of classes offer convenience |
| * methods for accessing formatter functionality. |
| * Of these, {@link String#format} is generally the most useful. |
| * {@link java.io.PrintStream} and {@link java.io.PrintWriter} both offer |
| * {@code format} and {@code printf} methods. |
| * <p> |
| * <i>Format strings</i> consist of plain text interspersed with format specifiers, such |
| * as {@code "name: %s weight: %03dkg\n"}. Being a Java string, the usual Java string literal |
| * backslash escapes are of course available. |
| * <p> |
| * <i>Format specifiers</i> (such as {@code "%s"} or {@code "%03d"} in the example) start with a |
| * {@code %} and describe how to format their corresponding argument. It includes an optional |
| * argument index, optional flags, an optional width, an optional precision, and a mandatory |
| * conversion type. |
| * In the example, {@code "%s"} has no flags, no width, and no precision, while |
| * {@code "%03d"} has the flag {@code 0}, the width 3, and no precision. |
| * <p> |
| * Not all combinations of argument index, flags, width, precision, and conversion type |
| * are valid. |
| * <p> |
| * <i>Argument index</i>. Normally, each format specifier consumes the next argument to |
| * {@code format}. |
| * For convenient localization, it's possible to reorder arguments so that they appear in a |
| * different order in the output than the order in which they were supplied. |
| * For example, {@code "%4$s"} formats the fourth argument ({@code 4$}) as a string ({@code s}). |
| * It's also possible to reuse an argument with {@code <}. For example, |
| * {@code format("%o %<d %<x", 64)} results in {@code "100 64 40"}. |
| * <p> |
| * <i>Flags</i>. The available flags are: |
| * <p> |
| * <table BORDER="1" WIDTH="100%" CELLPADDING="3" CELLSPACING="0" SUMMARY=""> |
| * <tr BGCOLOR="#CCCCFF" CLASS="TableHeadingColor"> <TD COLSPAN=4> <B>Flags</B> </TD> </tr> |
| * <tr> |
| * <td width="5%">{@code ,}</td> |
| * <td width="25%">Use grouping separators for large numbers. (Decimal only.)</td> |
| * <td width="30%">{@code format("%,d", 1024);}</td> |
| * <td width="30%">{@code 1,234}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code +}</td> |
| * <td width="25%">Always show sign. (Decimal only.)</td> |
| * <td width="30%">{@code format("%+d, %+4d", 5, 5);}</td> |
| * <td width="30%"><pre>+5, +5</pre></td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code }</td> |
| * <td width="25%">A space indicates that non-negative numbers |
| * should have a leading space. (Decimal only.)</td> |
| * <td width="30%">{@code format("x% d% 5d", 4, 4);}</td> |
| * <td width="30%"><pre>x 4 4</pre></td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code (}</td> |
| * <td width="25%">Put parentheses around negative numbers. (Decimal only.)</td> |
| * <td width="30%">{@code format("%(d, %(d, %(6d", 12, -12, -12);}</td> |
| * <td width="30%"><pre>12, (12), (12)</pre></td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code -}</td> |
| * <td width="25%">Left-justify. (Requires width.)</td> |
| * <td width="30%">{@code format("%-6dx", 5);}<br/>{@code format("%-3C, %3C", 'd', 0x65);}</td> |
| * <td width="30%"><pre>5 x</pre><br/><pre>D , E</pre></td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code 0}</td> |
| * <td width="25%">Pad the number with leading zeros. (Requires width.)</td> |
| * <td width="30%">{@code format("%07d, %03d", 4, 5555);}</td> |
| * <td width="30%">{@code 0000004, 5555}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code #}</td> |
| * <td width="25%">Alternate form. (Octal and hex only.) </td> |
| * <td width="30%">{@code format("%o %#o", 010, 010);}<br/>{@code format("%x %#x", 0x12, 0x12);}</td> |
| * <td width="30%">{@code 10 010}<br/>{@code 12 0x12}</td> |
| * </tr> |
| * </table> |
| * <p> |
| * <i>Width</i>. The width is a decimal integer specifying the minimum number of characters to be |
| * used to represent the argument. If the result would otherwise be shorter than the width, padding |
| * will be added (the exact details of which depend on the flags). Note that you can't use width to |
| * truncate a field, only to make it wider: see precision for control over the maximum width. |
| * <p> |
| * <i>Precision</i>. The precision is a {@code .} followed by a decimal integer, giving the minimum |
| * number of digits for {@code d}, {@code o}, {@code x}, or {@code X}; the minimum number of digits |
| * after the decimal point for {@code a}, {@code A}, {@code e}, {@code E}, {@code f}, or {@code F}; |
| * the maximum number of significant digits for {@code g} or {@code G}; or the maximum number of |
| * characters for {@code s} or {@code S}. |
| * <p> |
| * <i>Conversion type</i>. One or two characters describing how to interpret the argument. Most |
| * conversions are a single character, but date/time conversions all start with {@code t} and |
| * have a single extra character describing the desired output. |
| * <p> |
| * Many conversion types have a corresponding uppercase variant that converts its result to |
| * uppercase using the rules of the relevant locale (either the default or the locale set for |
| * this formatter). |
| * <p> |
| * This table shows the available single-character (non-date/time) conversion types: |
| * <table BORDER="1" WIDTH="100%" CELLPADDING="3" CELLSPACING="0" SUMMARY=""> |
| * <tr BGCOLOR="#CCCCFF" CLASS="TableHeadingColor"> |
| * <TD COLSPAN=4> |
| * <B>String conversions</B> |
| * <br> |
| * All types are acceptable arguments. Values of type {@link Formattable} have their |
| * {@code formatTo} method invoked; all other types use {@code toString}. |
| * </TD> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code s}</td> |
| * <td width="25%">String.</td> |
| * <td width="30%">{@code format("%s %s", "hello", "Hello");}</td> |
| * <td width="30%">{@code hello Hello}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code S}</td> |
| * <td width="25%">Uppercase string.</td> |
| * <td width="30%">{@code format("%S %S", "hello", "Hello");}</td> |
| * <td width="30%">{@code HELLO HELLO}</td> |
| * </tr> |
| * <tr BGCOLOR="#CCCCFF" CLASS="TableHeadingColor"> |
| * <TD COLSPAN=4> |
| * <B>Character conversions</B> |
| * <br> |
| * Byte, Character, Short, and Integer (and primitives that box to those types) are all acceptable |
| * as character arguments. Any other type is an error. |
| * </TD> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code c}</td> |
| * <td width="25%">Character.</td> |
| * <td width="30%">{@code format("%c %c", 'd', 'E');}</td> |
| * <td width="30%">{@code d E}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code C}</td> |
| * <td width="25%">Uppercase character.</td> |
| * <td width="30%">{@code format("%C %C", 'd', 'E');}</td> |
| * <td width="30%">{@code D E}</td> |
| * </tr> |
| * <tr BGCOLOR="#CCCCFF" CLASS="TableHeadingColor"> |
| * <TD COLSPAN=4> |
| * <B>Integer conversions</B> |
| * <br> |
| * Byte, Short, Integer, Long, and BigInteger (and primitives that box to those types) are all |
| * acceptable as integer arguments. Any other type is an error. |
| * </TD> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code d}</td> |
| * <td width="25%">Decimal.</td> |
| * <td width="30%">{@code format("%d", 26);}</td> |
| * <td width="30%">{@code 26}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code o}</td> |
| * <td width="25%">Octal.</td> |
| * <td width="30%">{@code format("%o", 032);}</td> |
| * <td width="30%">{@code 32}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code x}, {@code X}</td> |
| * <td width="25%">Hexadecimal.</td> |
| * <td width="30%">{@code format("%x %X", 0x1a, 0x1a);}</td> |
| * <td width="30%">{@code 1a 1A}</td> |
| * </tr> |
| * <tr BGCOLOR="#CCCCFF" CLASS="TableHeadingColor"> |
| * <TD COLSPAN=4><B>Floating-point conversions</B> |
| * <br> |
| * Float, Double, and BigDecimal (and primitives that box to those types) are all acceptable as |
| * floating-point arguments. Any other type is an error. |
| * </TD> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code f}</td> |
| * <td width="25%">Decimal floating point.</td> |
| * <td width="30%"><pre> |
| format("%f", 123.456f); |
| format("%.1f", 123.456f); |
| format("%1.5f", 123.456f); |
| format("%10f", 123.456f); |
| format("%6.0f", 123.456f);</td> |
| * <td width="30%" valign="top"><pre> |
| 123.456001 |
| 123.5 |
| 123.45600 |
| 123.456001 |
| 123</pre></td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code e}, {@code E}</td> |
| * <td width="25%">Engineering/exponential floating point.</td> |
| * <td width="30%"><pre> |
| format("%e", 123.456f); |
| format("%.1e", 123.456f); |
| format("%1.5E", 123.456f); |
| format("%10E", 123.456f); |
| format("%6.0E", 123.456f);</td> |
| * <td width="30%" valign="top"><pre> |
| 1.234560e+02 |
| 1.2e+02 |
| 1.23456E+02 |
| 1.234560E+02 |
| 1E+02</pre></td> |
| * </tr> |
| * <tr> |
| * <td width="5%" valign="top">{@code g}, {@code G}</td> |
| * <td width="25%" valign="top">Decimal or engineering, depending on the magnitude of the value.</td> |
| * <td width="30%" valign="top">{@code format("%g %g", 0.123, 0.0000123);}</td> |
| * <td width="30%" valign="top">{@code 0.123000 1.23000e-05}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code a}, {@code A}</td> |
| * <td width="25%">Hexadecimal floating point.</td> |
| * <td width="30%">{@code format("%a", 123.456f);}</td> |
| * <td width="30%">{@code 0x1.edd2f2p6}</td> |
| * </tr> |
| * <tr BGCOLOR="#CCCCFF" CLASS="TableHeadingColor"> |
| * <TD COLSPAN=4> |
| * <B>Boolean conversion</B> |
| * <br> |
| * Accepts Boolean values. {@code null} is considered false, and instances of all other |
| * types are considered true. |
| * </TD> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code b}, {@code B}</td> |
| * <td width="25%">Boolean.</td> |
| * <td width="30%">{@code format("%b %b", true, false);}<br>{@code format("%B %B", true, false);}<br>{@code format("%b", null);}<br>{@code format("%b", "hello");}</td> |
| * <td width="30%">{@code true false}<br>{@code TRUE FALSE}<br>{@code false}<br>{@code true}</td> |
| * </tr> |
| * <tr BGCOLOR="#CCCCFF" CLASS="TableHeadingColor"> |
| * <TD COLSPAN=4> |
| * <B>Hash code conversion</B> |
| * <br> |
| * Invokes {@code hashCode} on its argument, which may be of any type. |
| * </TD> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code h}, {@code H}</td> |
| * <td width="25%">Hexadecimal hash code.</td> |
| * <td width="30%">{@code format("%h", this);}<br>{@code format("%H", this);}<br>{@code format("%h", null);}</td> |
| * <td width="30%">{@code 190d11}<br>{@code 190D11}<br>{@code null}</td> |
| * </tr> |
| * <tr BGCOLOR="#CCCCFF" CLASS="TableHeadingColor"> |
| * <TD COLSPAN=4> |
| * <B>Zero-argument conversions</B></TD> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code %}</td> |
| * <td width="25%">A literal % character.</td> |
| * <td width="30%">{@code format("%d%%", 50);}</td> |
| * <td width="30%">{@code 50%}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code n}</td> |
| * <td width="25%">Newline. (The value of the "line.separator" system property}.)</td> |
| * <td width="30%">{@code format("first%nsecond");}</td> |
| * <td width="30%">{@code first\nsecond}</td> |
| * </tr> |
| * </table> |
| * <p> |
| * It's also possible to format dates and times with {@code Formatter}, though you should |
| * use {@link java.text.SimpleDateFormat} (probably via the factory methods in |
| * {@link java.text.DateFormat}) instead. |
| * The facilities offered by {@code Formatter} are low-level and place the burden of localization |
| * on the developer. Using {@link java.text.DateFormat#getDateInstance}, |
| * {@link java.text.DateFormat#getTimeInstance}, and |
| * {@link java.text.DateFormat#getDateTimeInstance} is preferable for dates and times that will be |
| * presented to a human. Those methods will select the best format strings for the user's locale. |
| * <p> |
| * The best non-localized form is <a href="http://en.wikipedia.org/wiki/ISO_8601">ISO 8601</a>, |
| * which you can get with {@code "%tF"} (2010-01-22), {@code "%tF %tR"} (2010-01-22 13:39), |
| * {@code "%tF %tT"} (2010-01-22 13:39:15), or {@code "%tF %tT%z"} (2010-01-22 13:39:15-0800). |
| * <p> |
| * This table shows the date/time conversions, but you should use {@link java.text.SimpleDateFormat} |
| * instead: |
| * <table BORDER="1" WIDTH="100%" CELLPADDING="3" CELLSPACING="0" SUMMARY=""> |
| * <tr BGCOLOR="#CCCCFF" CLASS="TableHeadingColor"> |
| * <TD COLSPAN=4><B>Date/time conversions</B> |
| * <br> |
| * Calendar, Date, and Long (representing milliseconds past the epoch) are all acceptable |
| * as date/time arguments. Any other type is an error. The epoch is 1970-01-01 00:00:00 UTC. |
| * </TD> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code ta}</td> |
| * <td width="25%">Localized weekday name (abbreviated).</td> |
| * <td width="30%">{@code format("%ta", cal, cal);}</td> |
| * <td width="30%">{@code Tue}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code tA}</td> |
| * <td width="25%">Localized weekday name (full).</td> |
| * <td width="30%">{@code format("%tA", cal, cal);}</td> |
| * <td width="30%">{@code Tuesday}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code tb}</td> |
| * <td width="25%">Localized month name (abbreviated).</td> |
| * <td width="30%">{@code format("%tb", cal);}</td> |
| * <td width="30%">{@code Apr}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code tB}</td> |
| * <td width="25%">Localized month name (full).</td> |
| * <td width="30%">{@code format("%tB", cal);}</td> |
| * <td width="30%">{@code April}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code tc}</td> |
| * <td width="25%">C library <i>asctime(3)</i>-like output. Do not use.</td> |
| * <td width="30%">{@code format("%tc", cal);}</td> |
| * <td width="30%">{@code Tue Apr 01 16:19:17 CEST 2008}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code tC}</td> |
| * <td width="25%">2-digit century.</td> |
| * <td width="30%">{@code format("%tC", cal);}</td> |
| * <td width="30%">{@code 20}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code td}</td> |
| * <td width="25%">2-digit day of month (01-31).</td> |
| * <td width="30%">{@code format("%td", cal);}</td> |
| * <td width="30%">{@code 01}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code tD}</td> |
| * <td width="25%">Ambiguous US date format (MM/DD/YY). Do not use.</td> |
| * <td width="30%">{@code format("%tD", cal);}</td> |
| * <td width="30%">{@code 04/01/08}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code te}</td> |
| * <td width="25%">Day of month (1-31).</td> |
| * <td width="30%">{@code format("%te", cal);}</td> |
| * <td width="30%">{@code 1}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code tF}</td> |
| * <td width="25%">Full date in ISO 8601 format (YYYY-MM-DD).</td> |
| * <td width="30%">{@code format("%tF", cal);}</td> |
| * <td width="30%">{@code 2008-04-01}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code th}</td> |
| * <td width="25%">Synonym for {@code %tb}.</td> |
| * <td width="30%"></td> |
| * <td width="30%"></td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code tH}</td> |
| * <td width="25%">24-hour hour of day (00-23).</td> |
| * <td width="30%">{@code format("%tH", cal);}</td> |
| * <td width="30%">{@code 16}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code tI}</td> |
| * <td width="25%">12-hour hour of day (01-12).</td> |
| * <td width="30%">{@code format("%tH", cal);}</td> |
| * <td width="30%">{@code 04}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code tj}</td> |
| * <td width="25%">3-digit day of year (001-366).</td> |
| * <td width="30%">{@code format("%tj", cal);}</td> |
| * <td width="30%">{@code 092}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code tk}</td> |
| * <td width="25%">24-hour hour of day (0-23).</td> |
| * <td width="30%">{@code format("%tH", cal);}</td> |
| * <td width="30%">{@code 16}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code tl}</td> |
| * <td width="25%">12-hour hour of day (1-12).</td> |
| * <td width="30%">{@code format("%tH", cal);}</td> |
| * <td width="30%">{@code 4}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code tL}</td> |
| * <td width="25%">Milliseconds.</td> |
| * <td width="30%">{@code format("%tL", cal);}</td> |
| * <td width="30%">{@code 359}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code tm}</td> |
| * <td width="25%">2-digit month of year (01-12).</td> |
| * <td width="30%">{@code format("%tm", cal);}</td> |
| * <td width="30%">{@code 04}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code tM}</td> |
| * <td width="25%">2-digit minute.</td> |
| * <td width="30%">{@code format("%tM", cal);}</td> |
| * <td width="30%">{@code 08}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code tN}</td> |
| * <td width="25%">Nanoseconds.</td> |
| * <td width="30%">{@code format("%tN", cal);}</td> |
| * <td width="30%">{@code 359000000}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code tp}</td> |
| * <td width="25%">a.m. or p.m.</td> |
| * <td width="30%">{@code format("%tp %Tp", cal, cal);}</td> |
| * <td width="30%">{@code pm PM}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code tQ}</td> |
| * <td width="25%">Milliseconds since the epoch.</td> |
| * <td width="30%">{@code format("%tQ", cal);}</td> |
| * <td width="30%">{@code 1207059412656}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code tr}</td> |
| * <td width="25%">Full 12-hour time ({@code %tI:%tM:%tS %Tp}).</td> |
| * <td width="30%">{@code format("%tr", cal);}</td> |
| * <td width="30%">{@code 04:15:32 PM}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code tR}</td> |
| * <td width="25%">Short 24-hour time ({@code %tH:%tM}).</td> |
| * <td width="30%">{@code format("%tR", cal);}</td> |
| * <td width="30%">{@code 16:15}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code ts}</td> |
| * <td width="25%">Seconds since the epoch.</td> |
| * <td width="30%">{@code format("%ts", cal);}</td> |
| * <td width="30%">{@code 1207059412}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code tS}</td> |
| * <td width="25%">2-digit seconds (00-60).</td> |
| * <td width="30%">{@code format("%tS", cal);}</td> |
| * <td width="30%">{@code 17}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code tT}</td> |
| * <td width="25%">Full 24-hour time ({@code %tH:%tM:%tS}).</td> |
| * <td width="30%">{@code format("%tT", cal);}</td> |
| * <td width="30%">{@code 16:15:32}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code ty}</td> |
| * <td width="25%">2-digit year (00-99).</td> |
| * <td width="30%">{@code format("%ty", cal);}</td> |
| * <td width="30%">{@code 08}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code tY}</td> |
| * <td width="25%">4-digit year.</td> |
| * <td width="30%">{@code format("%tY", cal);}</td> |
| * <td width="30%">{@code 2008}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code tz}</td> |
| * <td width="25%">Time zone GMT offset.</td> |
| * <td width="30%">{@code format("%tz", cal);}</td> |
| * <td width="30%">{@code +0100}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code tZ}</td> |
| * <td width="25%">Localized time zone abbreviation.</td> |
| * <td width="30%">{@code format("%tZ", cal);}</td> |
| * <td width="30%">{@code CEST}</td> |
| * </tr> |
| * </table> |
| * <p> |
| * As with the other conversions, date/time conversion has an uppercase format. Replacing |
| * {@code %t} with {@code %T} will uppercase the field according to the rules of the formatter's |
| * locale. |
| * <p><i>Number localization</i>. Some conversions use localized decimal digits rather than the |
| * usual ASCII digits. So formatting {@code 123} with {@code %d} will give 123 in English locales |
| * but ١٢٣ in appropriate Arabic locales, for example. This number localization |
| * occurs for the decimal integer conversion {@code %d}, the floating point conversions {@code %e}, |
| * {@code %f}, and {@code %g}, and all date/time {@code %t} or {@code %T} conversions, but no other |
| * conversions. |
| * <p><i>Thread safety</i>. Formatter is not thread-safe. |
| * |
| * @since 1.5 |
| * @see java.text.DateFormat |
| * @see Formattable |
| * @see java.text.SimpleDateFormat |
| */ |
| public final class Formatter implements Closeable, Flushable { |
| private static final char[] ZEROS = new char[] { '0', '0', '0', '0', '0', '0', '0', '0', '0' }; |
| |
| /** |
| * The enumeration giving the available styles for formatting very large |
| * decimal numbers. |
| */ |
| public enum BigDecimalLayoutForm { |
| /** |
| * Use scientific style for BigDecimals. |
| */ |
| SCIENTIFIC, |
| /** |
| * Use normal decimal/float style for BigDecimals. |
| */ |
| DECIMAL_FLOAT |
| } |
| |
| // User-settable parameters. |
| private Appendable out; |
| private Locale locale; |
| |
| // Implementation details. |
| private Object arg; |
| private boolean closed = false; |
| private FormatToken formatToken; |
| private IOException lastIOException; |
| private LocaleData localeData; |
| |
| private static class CachedDecimalFormat { |
| public NativeDecimalFormat decimalFormat; |
| public LocaleData currentLocaleData; |
| public String currentPattern; |
| |
| public CachedDecimalFormat() { |
| } |
| |
| public NativeDecimalFormat update(LocaleData localeData, String pattern) { |
| if (decimalFormat == null) { |
| currentPattern = pattern; |
| currentLocaleData = localeData; |
| decimalFormat = new NativeDecimalFormat(currentPattern, currentLocaleData); |
| } |
| if (!pattern.equals(currentPattern)) { |
| decimalFormat.applyPattern(pattern); |
| currentPattern = pattern; |
| } |
| if (localeData != currentLocaleData) { |
| decimalFormat.setDecimalFormatSymbols(localeData); |
| currentLocaleData = localeData; |
| } |
| return decimalFormat; |
| } |
| } |
| |
| private static final ThreadLocal<CachedDecimalFormat> cachedDecimalFormat = new ThreadLocal<CachedDecimalFormat>() { |
| @Override protected CachedDecimalFormat initialValue() { |
| return new CachedDecimalFormat(); |
| } |
| }; |
| |
| /** |
| * Creates a native peer if we don't already have one, or reconfigures an existing one. |
| * This means we get to reuse the peer in cases like "x=%.2f y=%.2f". |
| */ |
| private NativeDecimalFormat getDecimalFormat(String pattern) { |
| return cachedDecimalFormat.get().update(localeData, pattern); |
| } |
| |
| /** |
| * Constructs a {@code Formatter}. |
| * |
| * <p>The output is written to a {@code StringBuilder} which can be acquired by invoking |
| * {@link #out()} and whose content can be obtained by calling {@code toString}. |
| * |
| * <p>The {@code Locale} used is the user's default locale. |
| * See "<a href="../util/Locale.html#default_locale">Be wary of the default locale</a>". |
| */ |
| public Formatter() { |
| this(new StringBuilder(), Locale.getDefault()); |
| } |
| |
| /** |
| * Constructs a {@code Formatter} whose output will be written to the |
| * specified {@code Appendable}. |
| * |
| * <p>The {@code Locale} used is the user's default locale. |
| * See "<a href="../util/Locale.html#default_locale">Be wary of the default locale</a>". |
| * |
| * @param a |
| * the output destination of the {@code Formatter}. If {@code a} is {@code null}, |
| * then a {@code StringBuilder} will be used. |
| */ |
| public Formatter(Appendable a) { |
| this(a, Locale.getDefault()); |
| } |
| |
| /** |
| * Constructs a {@code Formatter} with the specified {@code Locale}. |
| * |
| * <p>The output is written to a {@code StringBuilder} which can be acquired by invoking |
| * {@link #out()} and whose content can be obtained by calling {@code toString}. |
| * |
| * @param l |
| * the {@code Locale} of the {@code Formatter}. If {@code l} is {@code null}, |
| * then no localization will be used. |
| */ |
| public Formatter(Locale l) { |
| this(new StringBuilder(), l); |
| } |
| |
| /** |
| * Constructs a {@code Formatter} with the specified {@code Locale} |
| * and whose output will be written to the |
| * specified {@code Appendable}. |
| * |
| * @param a |
| * the output destination of the {@code Formatter}. If {@code a} is {@code null}, |
| * then a {@code StringBuilder} will be used. |
| * @param l |
| * the {@code Locale} of the {@code Formatter}. If {@code l} is {@code null}, |
| * then no localization will be used. |
| */ |
| public Formatter(Appendable a, Locale l) { |
| if (a == null) { |
| out = new StringBuilder(); |
| } else { |
| out = a; |
| } |
| locale = l; |
| } |
| |
| /** |
| * Constructs a {@code Formatter} whose output is written to the specified file. |
| * |
| * <p>The charset of the {@code Formatter} is the default charset. |
| * |
| * <p>The {@code Locale} used is the user's default locale. |
| * See "<a href="../util/Locale.html#default_locale">Be wary of the default locale</a>". |
| * |
| * @param fileName |
| * the filename of the file that is used as the output |
| * destination for the {@code Formatter}. The file will be truncated to |
| * zero size if the file exists, or else a new file will be |
| * created. The output of the {@code Formatter} is buffered. |
| * @throws FileNotFoundException |
| * if the filename does not denote a normal and writable file, |
| * or if a new file cannot be created, or if any error arises when |
| * opening or creating the file. |
| */ |
| public Formatter(String fileName) throws FileNotFoundException { |
| this(new File(fileName)); |
| |
| } |
| |
| /** |
| * Constructs a {@code Formatter} whose output is written to the specified file. |
| * |
| * <p>The {@code Locale} used is the user's default locale. |
| * See "<a href="../util/Locale.html#default_locale">Be wary of the default locale</a>". |
| * |
| * @param fileName |
| * the filename of the file that is used as the output |
| * destination for the {@code Formatter}. The file will be truncated to |
| * zero size if the file exists, or else a new file will be |
| * created. The output of the {@code Formatter} is buffered. |
| * @param csn |
| * the name of the charset for the {@code Formatter}. |
| * @throws FileNotFoundException |
| * if the filename does not denote a normal and writable file, |
| * or if a new file cannot be created, or if any error arises when |
| * opening or creating the file. |
| * @throws UnsupportedEncodingException |
| * if the charset with the specified name is not supported. |
| */ |
| public Formatter(String fileName, String csn) throws FileNotFoundException, |
| UnsupportedEncodingException { |
| this(new File(fileName), csn); |
| } |
| |
| /** |
| * Constructs a {@code Formatter} with the given {@code Locale} and charset, |
| * and whose output is written to the specified file. |
| * |
| * @param fileName |
| * the filename of the file that is used as the output |
| * destination for the {@code Formatter}. The file will be truncated to |
| * zero size if the file exists, or else a new file will be |
| * created. The output of the {@code Formatter} is buffered. |
| * @param csn |
| * the name of the charset for the {@code Formatter}. |
| * @param l |
| * the {@code Locale} of the {@code Formatter}. If {@code l} is {@code null}, |
| * then no localization will be used. |
| * @throws FileNotFoundException |
| * if the filename does not denote a normal and writable file, |
| * or if a new file cannot be created, or if any error arises when |
| * opening or creating the file. |
| * @throws UnsupportedEncodingException |
| * if the charset with the specified name is not supported. |
| */ |
| public Formatter(String fileName, String csn, Locale l) |
| throws FileNotFoundException, UnsupportedEncodingException { |
| |
| this(new File(fileName), csn, l); |
| } |
| |
| /** |
| * Constructs a {@code Formatter} whose output is written to the specified {@code File}. |
| * |
| * The charset of the {@code Formatter} is the default charset. |
| * |
| * <p>The {@code Locale} used is the user's default locale. |
| * See "<a href="../util/Locale.html#default_locale">Be wary of the default locale</a>". |
| * |
| * @param file |
| * the {@code File} that is used as the output destination for the |
| * {@code Formatter}. The {@code File} will be truncated to zero size if the {@code File} |
| * exists, or else a new {@code File} will be created. The output of the |
| * {@code Formatter} is buffered. |
| * @throws FileNotFoundException |
| * if the {@code File} is not a normal and writable {@code File}, or if a |
| * new {@code File} cannot be created, or if any error rises when opening or |
| * creating the {@code File}. |
| */ |
| public Formatter(File file) throws FileNotFoundException { |
| this(new FileOutputStream(file)); |
| } |
| |
| /** |
| * Constructs a {@code Formatter} with the given charset, |
| * and whose output is written to the specified {@code File}. |
| * |
| * <p>The {@code Locale} used is the user's default locale. |
| * See "<a href="../util/Locale.html#default_locale">Be wary of the default locale</a>". |
| * |
| * @param file |
| * the {@code File} that is used as the output destination for the |
| * {@code Formatter}. The {@code File} will be truncated to zero size if the {@code File} |
| * exists, or else a new {@code File} will be created. The output of the |
| * {@code Formatter} is buffered. |
| * @param csn |
| * the name of the charset for the {@code Formatter}. |
| * @throws FileNotFoundException |
| * if the {@code File} is not a normal and writable {@code File}, or if a |
| * new {@code File} cannot be created, or if any error rises when opening or |
| * creating the {@code File}. |
| * @throws UnsupportedEncodingException |
| * if the charset with the specified name is not supported. |
| */ |
| public Formatter(File file, String csn) throws FileNotFoundException, |
| UnsupportedEncodingException { |
| this(file, csn, Locale.getDefault()); |
| } |
| |
| /** |
| * Constructs a {@code Formatter} with the given {@code Locale} and charset, |
| * and whose output is written to the specified {@code File}. |
| * |
| * @param file |
| * the {@code File} that is used as the output destination for the |
| * {@code Formatter}. The {@code File} will be truncated to zero size if the {@code File} |
| * exists, or else a new {@code File} will be created. The output of the |
| * {@code Formatter} is buffered. |
| * @param csn |
| * the name of the charset for the {@code Formatter}. |
| * @param l |
| * the {@code Locale} of the {@code Formatter}. If {@code l} is {@code null}, |
| * then no localization will be used. |
| * @throws FileNotFoundException |
| * if the {@code File} is not a normal and writable {@code File}, or if a |
| * new {@code File} cannot be created, or if any error rises when opening or |
| * creating the {@code File}. |
| * @throws UnsupportedEncodingException |
| * if the charset with the specified name is not supported. |
| */ |
| public Formatter(File file, String csn, Locale l) |
| throws FileNotFoundException, UnsupportedEncodingException { |
| FileOutputStream fout = null; |
| try { |
| fout = new FileOutputStream(file); |
| out = new BufferedWriter(new OutputStreamWriter(fout, csn)); |
| } catch (RuntimeException e) { |
| IoUtils.closeQuietly(fout); |
| throw e; |
| } catch (UnsupportedEncodingException e) { |
| IoUtils.closeQuietly(fout); |
| throw e; |
| } |
| |
| locale = l; |
| } |
| |
| /** |
| * Constructs a {@code Formatter} whose output is written to the specified {@code OutputStream}. |
| * |
| * <p>The charset of the {@code Formatter} is the default charset. |
| * |
| * <p>The {@code Locale} used is the user's default locale. |
| * See "<a href="../util/Locale.html#default_locale">Be wary of the default locale</a>". |
| * |
| * @param os |
| * the stream to be used as the destination of the {@code Formatter}. |
| */ |
| public Formatter(OutputStream os) { |
| out = new BufferedWriter(new OutputStreamWriter(os, Charset.defaultCharset())); |
| locale = Locale.getDefault(); |
| } |
| |
| /** |
| * Constructs a {@code Formatter} with the given charset, |
| * and whose output is written to the specified {@code OutputStream}. |
| * |
| * <p>The {@code Locale} used is the user's default locale. |
| * See "<a href="../util/Locale.html#default_locale">Be wary of the default locale</a>". |
| * |
| * @param os |
| * the stream to be used as the destination of the {@code Formatter}. |
| * @param csn |
| * the name of the charset for the {@code Formatter}. |
| * @throws UnsupportedEncodingException |
| * if the charset with the specified name is not supported. |
| */ |
| public Formatter(OutputStream os, String csn) throws UnsupportedEncodingException { |
| this(os, csn, Locale.getDefault()); |
| } |
| |
| /** |
| * Constructs a {@code Formatter} with the given {@code Locale} and charset, |
| * and whose output is written to the specified {@code OutputStream}. |
| * |
| * @param os |
| * the stream to be used as the destination of the {@code Formatter}. |
| * @param csn |
| * the name of the charset for the {@code Formatter}. |
| * @param l |
| * the {@code Locale} of the {@code Formatter}. If {@code l} is {@code null}, |
| * then no localization will be used. |
| * @throws UnsupportedEncodingException |
| * if the charset with the specified name is not supported. |
| */ |
| public Formatter(OutputStream os, String csn, Locale l) throws UnsupportedEncodingException { |
| out = new BufferedWriter(new OutputStreamWriter(os, csn)); |
| locale = l; |
| } |
| |
| /** |
| * Constructs a {@code Formatter} whose output is written to the specified {@code PrintStream}. |
| * |
| * <p>The charset of the {@code Formatter} is the default charset. |
| * |
| * <p>The {@code Locale} used is the user's default locale. |
| * See "<a href="../util/Locale.html#default_locale">Be wary of the default locale</a>". |
| * |
| * @param ps |
| * the {@code PrintStream} used as destination of the {@code Formatter}. If |
| * {@code ps} is {@code null}, then a {@code NullPointerException} will |
| * be raised. |
| */ |
| public Formatter(PrintStream ps) { |
| if (ps == null) { |
| throw new NullPointerException("ps == null"); |
| } |
| out = ps; |
| locale = Locale.getDefault(); |
| } |
| |
| private void checkNotClosed() { |
| if (closed) { |
| throw new FormatterClosedException(); |
| } |
| } |
| |
| /** |
| * Returns the {@code Locale} of the {@code Formatter}. |
| * |
| * @return the {@code Locale} for the {@code Formatter} or {@code null} for no {@code Locale}. |
| * @throws FormatterClosedException |
| * if the {@code Formatter} has been closed. |
| */ |
| public Locale locale() { |
| checkNotClosed(); |
| return locale; |
| } |
| |
| /** |
| * Returns the output destination of the {@code Formatter}. |
| * |
| * @return the output destination of the {@code Formatter}. |
| * @throws FormatterClosedException |
| * if the {@code Formatter} has been closed. |
| */ |
| public Appendable out() { |
| checkNotClosed(); |
| return out; |
| } |
| |
| /** |
| * Returns the content by calling the {@code toString()} method of the output |
| * destination. |
| * |
| * @return the content by calling the {@code toString()} method of the output |
| * destination. |
| * @throws FormatterClosedException |
| * if the {@code Formatter} has been closed. |
| */ |
| @Override |
| public String toString() { |
| checkNotClosed(); |
| return out.toString(); |
| } |
| |
| /** |
| * Flushes the {@code Formatter}. If the output destination is {@link Flushable}, |
| * then the method {@code flush()} will be called on that destination. |
| * |
| * @throws FormatterClosedException |
| * if the {@code Formatter} has been closed. |
| */ |
| public void flush() { |
| checkNotClosed(); |
| if (out instanceof Flushable) { |
| try { |
| ((Flushable) out).flush(); |
| } catch (IOException e) { |
| lastIOException = e; |
| } |
| } |
| } |
| |
| /** |
| * Closes the {@code Formatter}. If the output destination is {@link Closeable}, |
| * then the method {@code close()} will be called on that destination. |
| * |
| * If the {@code Formatter} has been closed, then calling the this method will have no |
| * effect. |
| * |
| * Any method but the {@link #ioException()} that is called after the |
| * {@code Formatter} has been closed will raise a {@code FormatterClosedException}. |
| */ |
| public void close() { |
| if (!closed) { |
| closed = true; |
| try { |
| if (out instanceof Closeable) { |
| ((Closeable) out).close(); |
| } |
| } catch (IOException e) { |
| lastIOException = e; |
| } |
| } |
| } |
| |
| /** |
| * Returns the last {@code IOException} thrown by the {@code Formatter}'s output |
| * destination. If the {@code append()} method of the destination does not throw |
| * {@code IOException}s, the {@code ioException()} method will always return {@code null}. |
| * |
| * @return the last {@code IOException} thrown by the {@code Formatter}'s output |
| * destination. |
| */ |
| public IOException ioException() { |
| return lastIOException; |
| } |
| |
| /** |
| * Writes a formatted string to the output destination of the {@code Formatter}. |
| * |
| * @param format |
| * a format string. |
| * @param args |
| * the arguments list used in the {@code format()} method. If there are |
| * more arguments than those specified by the format string, then |
| * the additional arguments are ignored. |
| * @return this {@code Formatter}. |
| * @throws IllegalFormatException |
| * if the format string is illegal or incompatible with the |
| * arguments, or if fewer arguments are sent than those required by |
| * the format string, or any other illegal situation. |
| * @throws FormatterClosedException |
| * if the {@code Formatter} has been closed. |
| */ |
| public Formatter format(String format, Object... args) { |
| return format(this.locale, format, args); |
| } |
| |
| /** |
| * Writes a formatted string to the output destination of the {@code Formatter}. |
| * |
| * @param l |
| * the {@code Locale} used in the method. If {@code locale} is |
| * {@code null}, then no localization will be applied. This |
| * parameter does not change this Formatter's default {@code Locale} |
| * as specified during construction, and only applies for the |
| * duration of this call. |
| * @param format |
| * a format string. |
| * @param args |
| * the arguments list used in the {@code format()} method. If there are |
| * more arguments than those specified by the format string, then |
| * the additional arguments are ignored. |
| * @return this {@code Formatter}. |
| * @throws IllegalFormatException |
| * if the format string is illegal or incompatible with the |
| * arguments, or if fewer arguments are sent than those required by |
| * the format string, or any other illegal situation. |
| * @throws FormatterClosedException |
| * if the {@code Formatter} has been closed. |
| */ |
| public Formatter format(Locale l, String format, Object... args) { |
| Locale originalLocale = locale; |
| try { |
| this.locale = (l == null ? Locale.US : l); |
| this.localeData = LocaleData.get(locale); |
| doFormat(format, args); |
| } finally { |
| this.locale = originalLocale; |
| } |
| return this; |
| } |
| |
| private void doFormat(String format, Object... args) { |
| checkNotClosed(); |
| |
| FormatSpecifierParser fsp = new FormatSpecifierParser(format); |
| int currentObjectIndex = 0; |
| Object lastArgument = null; |
| boolean hasLastArgumentSet = false; |
| |
| int length = format.length(); |
| int i = 0; |
| while (i < length) { |
| // Find the maximal plain-text sequence... |
| int plainTextStart = i; |
| int nextPercent = format.indexOf('%', i); |
| int plainTextEnd = (nextPercent == -1) ? length : nextPercent; |
| // ...and output it. |
| if (plainTextEnd > plainTextStart) { |
| outputCharSequence(format, plainTextStart, plainTextEnd); |
| } |
| i = plainTextEnd; |
| // Do we have a format specifier? |
| if (i < length) { |
| FormatToken token = fsp.parseFormatToken(i + 1); |
| |
| Object argument = null; |
| if (token.requireArgument()) { |
| int index = token.getArgIndex() == FormatToken.UNSET ? currentObjectIndex++ : token.getArgIndex(); |
| argument = getArgument(args, index, fsp, lastArgument, hasLastArgumentSet); |
| lastArgument = argument; |
| hasLastArgumentSet = true; |
| } |
| |
| CharSequence substitution = transform(token, argument); |
| // The substitution is null if we called Formattable.formatTo. |
| if (substitution != null) { |
| outputCharSequence(substitution, 0, substitution.length()); |
| } |
| i = fsp.i; |
| } |
| } |
| } |
| |
| // Fixes http://code.google.com/p/android/issues/detail?id=1767. |
| private void outputCharSequence(CharSequence cs, int start, int end) { |
| try { |
| out.append(cs, start, end); |
| } catch (IOException e) { |
| lastIOException = e; |
| } |
| } |
| |
| private Object getArgument(Object[] args, int index, FormatSpecifierParser fsp, |
| Object lastArgument, boolean hasLastArgumentSet) { |
| if (index == FormatToken.LAST_ARGUMENT_INDEX && !hasLastArgumentSet) { |
| throw new MissingFormatArgumentException("<"); |
| } |
| |
| if (args == null) { |
| return null; |
| } |
| |
| if (index >= args.length) { |
| throw new MissingFormatArgumentException(fsp.getFormatSpecifierText()); |
| } |
| |
| if (index == FormatToken.LAST_ARGUMENT_INDEX) { |
| return lastArgument; |
| } |
| |
| return args[index]; |
| } |
| |
| /* |
| * Complete details of a single format specifier parsed from a format string. |
| */ |
| private static class FormatToken { |
| static final int LAST_ARGUMENT_INDEX = -2; |
| |
| static final int UNSET = -1; |
| |
| static final int FLAGS_UNSET = 0; |
| |
| static final int DEFAULT_PRECISION = 6; |
| |
| static final int FLAG_ZERO = 1 << 4; |
| |
| private int argIndex = UNSET; |
| |
| // These have package access for performance. They used to be represented by an int bitmask |
| // and accessed via methods, but Android's JIT doesn't yet do a good job of such code. |
| // Direct field access, on the other hand, is fast. |
| boolean flagComma; |
| boolean flagMinus; |
| boolean flagParenthesis; |
| boolean flagPlus; |
| boolean flagSharp; |
| boolean flagSpace; |
| boolean flagZero; |
| |
| private char conversionType = (char) UNSET; |
| private char dateSuffix; |
| |
| private int precision = UNSET; |
| private int width = UNSET; |
| |
| private StringBuilder strFlags; |
| |
| // Tests whether there were no flags, no width, and no precision specified. |
| boolean isDefault() { |
| return !flagComma && !flagMinus && !flagParenthesis && !flagPlus && !flagSharp && |
| !flagSpace && !flagZero && width == UNSET && precision == UNSET; |
| } |
| |
| boolean isPrecisionSet() { |
| return precision != UNSET; |
| } |
| |
| int getArgIndex() { |
| return argIndex; |
| } |
| |
| void setArgIndex(int index) { |
| argIndex = index; |
| } |
| |
| int getWidth() { |
| return width; |
| } |
| |
| void setWidth(int width) { |
| this.width = width; |
| } |
| |
| int getPrecision() { |
| return precision; |
| } |
| |
| void setPrecision(int precise) { |
| this.precision = precise; |
| } |
| |
| String getStrFlags() { |
| return (strFlags != null) ? strFlags.toString() : ""; |
| } |
| |
| /* |
| * Sets qualified char as one of the flags. If the char is qualified, |
| * sets it as a flag and returns true. Or else returns false. |
| */ |
| boolean setFlag(int ch) { |
| boolean dupe = false; |
| switch (ch) { |
| case ',': |
| dupe = flagComma; |
| flagComma = true; |
| break; |
| case '-': |
| dupe = flagMinus; |
| flagMinus = true; |
| break; |
| case '(': |
| dupe = flagParenthesis; |
| flagParenthesis = true; |
| break; |
| case '+': |
| dupe = flagPlus; |
| flagPlus = true; |
| break; |
| case '#': |
| dupe = flagSharp; |
| flagSharp = true; |
| break; |
| case ' ': |
| dupe = flagSpace; |
| flagSpace = true; |
| break; |
| case '0': |
| dupe = flagZero; |
| flagZero = true; |
| break; |
| default: |
| return false; |
| } |
| if (dupe) { |
| // The RI documentation implies we're supposed to report all the flags, not just |
| // the first duplicate, but the RI behaves the same as we do. |
| throw new DuplicateFormatFlagsException(String.valueOf(ch)); |
| } |
| if (strFlags == null) { |
| strFlags = new StringBuilder(7); // There are seven possible flags. |
| } |
| strFlags.append((char) ch); |
| return true; |
| } |
| |
| char getConversionType() { |
| return conversionType; |
| } |
| |
| void setConversionType(char c) { |
| conversionType = c; |
| } |
| |
| char getDateSuffix() { |
| return dateSuffix; |
| } |
| |
| void setDateSuffix(char c) { |
| dateSuffix = c; |
| } |
| |
| boolean requireArgument() { |
| return conversionType != '%' && conversionType != 'n'; |
| } |
| |
| void checkFlags(Object arg) { |
| // Work out which flags are allowed. |
| boolean allowComma = false; |
| boolean allowMinus = true; |
| boolean allowParenthesis = false; |
| boolean allowPlus = false; |
| boolean allowSharp = false; |
| boolean allowSpace = false; |
| boolean allowZero = false; |
| // Precision and width? |
| boolean allowPrecision = true; |
| boolean allowWidth = true; |
| // Argument? |
| boolean allowArgument = true; |
| switch (conversionType) { |
| // Character and date/time. |
| case 'c': case 'C': case 't': case 'T': |
| // Only '-' is allowed. |
| allowPrecision = false; |
| break; |
| |
| // String. |
| case 's': case 'S': |
| if (arg instanceof Formattable) { |
| allowSharp = true; |
| } |
| break; |
| |
| // Floating point. |
| case 'g': case 'G': |
| allowComma = allowParenthesis = allowPlus = allowSpace = allowZero = true; |
| break; |
| case 'f': |
| allowComma = allowParenthesis = allowPlus = allowSharp = allowSpace = allowZero = true; |
| break; |
| case 'e': case 'E': |
| allowParenthesis = allowPlus = allowSharp = allowSpace = allowZero = true; |
| break; |
| case 'a': case 'A': |
| allowPlus = allowSharp = allowSpace = allowZero = true; |
| break; |
| |
| // Integral. |
| case 'd': |
| allowComma = allowParenthesis = allowPlus = allowSpace = allowZero = true; |
| allowPrecision = false; |
| break; |
| case 'o': case 'x': case 'X': |
| allowSharp = allowZero = true; |
| if (arg == null || arg instanceof BigInteger) { |
| allowParenthesis = allowPlus = allowSpace = true; |
| } |
| allowPrecision = false; |
| break; |
| |
| // Special. |
| case 'n': |
| // Nothing is allowed. |
| allowMinus = false; |
| allowArgument = allowPrecision = allowWidth = false; |
| break; |
| case '%': |
| // The only flag allowed is '-', and no argument or precision is allowed. |
| allowArgument = false; |
| allowPrecision = false; |
| break; |
| |
| // Booleans and hash codes. |
| case 'b': case 'B': case 'h': case 'H': |
| break; |
| |
| default: |
| throw unknownFormatConversionException(); |
| } |
| |
| // Check for disallowed flags. |
| String mismatch = null; |
| if (!allowComma && flagComma) { |
| mismatch = ","; |
| } else if (!allowMinus && flagMinus) { |
| mismatch = "-"; |
| } else if (!allowParenthesis && flagParenthesis) { |
| mismatch = "("; |
| } else if (!allowPlus && flagPlus) { |
| mismatch = "+"; |
| } else if (!allowSharp && flagSharp) { |
| mismatch = "#"; |
| } else if (!allowSpace && flagSpace) { |
| mismatch = " "; |
| } else if (!allowZero && flagZero) { |
| mismatch = "0"; |
| } |
| if (mismatch != null) { |
| if (conversionType == 'n') { |
| // For no good reason, %n is a special case... |
| throw new IllegalFormatFlagsException(mismatch); |
| } else { |
| throw new FormatFlagsConversionMismatchException(mismatch, conversionType); |
| } |
| } |
| |
| // Check for a missing width with flags that require a width. |
| if ((flagMinus || flagZero) && width == UNSET) { |
| throw new MissingFormatWidthException("-" + conversionType); |
| } |
| |
| // Check that no-argument conversion types don't have an argument. |
| // Note: the RI doesn't enforce this. |
| if (!allowArgument && argIndex != UNSET) { |
| throw new IllegalFormatFlagsException("%" + conversionType + |
| " doesn't take an argument"); |
| } |
| |
| // Check that we don't have a precision or width where they're not allowed. |
| if (!allowPrecision && precision != UNSET) { |
| throw new IllegalFormatPrecisionException(precision); |
| } |
| if (!allowWidth && width != UNSET) { |
| throw new IllegalFormatWidthException(width); |
| } |
| |
| // Some combinations make no sense... |
| if (flagPlus && flagSpace) { |
| throw new IllegalFormatFlagsException("the '+' and ' ' flags are incompatible"); |
| } |
| if (flagMinus && flagZero) { |
| throw new IllegalFormatFlagsException("the '-' and '0' flags are incompatible"); |
| } |
| } |
| |
| public UnknownFormatConversionException unknownFormatConversionException() { |
| if (conversionType == 't' || conversionType == 'T') { |
| throw new UnknownFormatConversionException(String.format("%c%c", |
| conversionType, dateSuffix)); |
| } |
| throw new UnknownFormatConversionException(String.valueOf(conversionType)); |
| } |
| } |
| |
| /* |
| * Gets the formatted string according to the format token and the |
| * argument. |
| */ |
| private CharSequence transform(FormatToken token, Object argument) { |
| this.formatToken = token; |
| this.arg = argument; |
| |
| // There are only two format specifiers that matter: "%d" and "%s". |
| // Nothing else is common in the wild. We fast-path these two to |
| // avoid the heavyweight machinery needed to cope with flags, width, |
| // and precision. |
| if (token.isDefault()) { |
| switch (token.getConversionType()) { |
| case 's': |
| if (arg == null) { |
| return "null"; |
| } else if (!(arg instanceof Formattable)) { |
| return arg.toString(); |
| } |
| break; |
| case 'd': |
| boolean needLocalizedDigits = (localeData.zeroDigit != '0'); |
| if (out instanceof StringBuilder && !needLocalizedDigits) { |
| if (arg instanceof Integer || arg instanceof Short || arg instanceof Byte) { |
| IntegralToString.appendInt((StringBuilder) out, ((Number) arg).intValue()); |
| return null; |
| } else if (arg instanceof Long) { |
| IntegralToString.appendLong((StringBuilder) out, ((Long) arg).longValue()); |
| return null; |
| } |
| } |
| if (arg instanceof Integer || arg instanceof Long || arg instanceof Short || arg instanceof Byte) { |
| String result = arg.toString(); |
| return needLocalizedDigits ? localizeDigits(result) : result; |
| } |
| } |
| } |
| |
| formatToken.checkFlags(arg); |
| CharSequence result; |
| switch (token.getConversionType()) { |
| case 'B': case 'b': |
| result = transformFromBoolean(); |
| break; |
| case 'H': case 'h': |
| result = transformFromHashCode(); |
| break; |
| case 'S': case 's': |
| result = transformFromString(); |
| break; |
| case 'C': case 'c': |
| result = transformFromCharacter(); |
| break; |
| case 'd': case 'o': case 'x': case 'X': |
| if (arg == null || arg instanceof BigInteger) { |
| result = transformFromBigInteger(); |
| } else { |
| result = transformFromInteger(); |
| } |
| break; |
| case 'A': case 'a': case 'E': case 'e': case 'f': case 'G': case 'g': |
| result = transformFromFloat(); |
| break; |
| case '%': |
| result = transformFromPercent(); |
| break; |
| case 'n': |
| result = System.lineSeparator(); |
| break; |
| case 't': case 'T': |
| result = transformFromDateTime(); |
| break; |
| default: |
| throw token.unknownFormatConversionException(); |
| } |
| |
| if (Character.isUpperCase(token.getConversionType())) { |
| if (result != null) { |
| result = result.toString().toUpperCase(locale); |
| } |
| } |
| return result; |
| } |
| |
| private IllegalFormatConversionException badArgumentType() { |
| throw new IllegalFormatConversionException(formatToken.getConversionType(), arg.getClass()); |
| } |
| |
| /** |
| * Returns a CharSequence corresponding to {@code s} with all the ASCII digits replaced |
| * by digits appropriate to this formatter's locale. Other characters remain unchanged. |
| */ |
| private CharSequence localizeDigits(CharSequence s) { |
| int length = s.length(); |
| int offsetToLocalizedDigits = localeData.zeroDigit - '0'; |
| StringBuilder result = new StringBuilder(length); |
| for (int i = 0; i < length; ++i) { |
| char ch = s.charAt(i); |
| if (ch >= '0' && ch <= '9') { |
| ch += offsetToLocalizedDigits; |
| } |
| result.append(ch); |
| } |
| return result; |
| } |
| |
| /** |
| * Inserts the grouping separator every 3 digits. DecimalFormat lets you configure grouping |
| * size, but you can't access that from Formatter, and the default is every 3 digits. |
| */ |
| private CharSequence insertGrouping(CharSequence s) { |
| StringBuilder result = new StringBuilder(s.length() + s.length()/3); |
| |
| // A leading '-' doesn't want to be included in the grouping. |
| int digitsLength = s.length(); |
| int i = 0; |
| if (s.charAt(0) == '-') { |
| --digitsLength; |
| ++i; |
| result.append('-'); |
| } |
| |
| // Append the digits that come before the first separator. |
| int headLength = digitsLength % 3; |
| if (headLength == 0) { |
| headLength = 3; |
| } |
| result.append(s, i, i + headLength); |
| i += headLength; |
| |
| // Append the remaining groups. |
| for (; i < s.length(); i += 3) { |
| result.append(localeData.groupingSeparator); |
| result.append(s, i, i + 3); |
| } |
| return result; |
| } |
| |
| private CharSequence transformFromBoolean() { |
| CharSequence result; |
| if (arg instanceof Boolean) { |
| result = arg.toString(); |
| } else if (arg == null) { |
| result = "false"; |
| } else { |
| result = "true"; |
| } |
| return padding(result, 0); |
| } |
| |
| private CharSequence transformFromHashCode() { |
| CharSequence result; |
| if (arg == null) { |
| result = "null"; |
| } else { |
| result = Integer.toHexString(arg.hashCode()); |
| } |
| return padding(result, 0); |
| } |
| |
| private CharSequence transformFromString() { |
| if (arg instanceof Formattable) { |
| int flags = 0; |
| if (formatToken.flagMinus) { |
| flags |= FormattableFlags.LEFT_JUSTIFY; |
| } |
| if (formatToken.flagSharp) { |
| flags |= FormattableFlags.ALTERNATE; |
| } |
| if (Character.isUpperCase(formatToken.getConversionType())) { |
| flags |= FormattableFlags.UPPERCASE; |
| } |
| ((Formattable) arg).formatTo(this, flags, formatToken.getWidth(), |
| formatToken.getPrecision()); |
| // all actions have been taken out in the |
| // Formattable.formatTo, thus there is nothing to do, just |
| // returns null, which tells the Parser to add nothing to the |
| // output. |
| return null; |
| } |
| CharSequence result = arg != null ? arg.toString() : "null"; |
| return padding(result, 0); |
| } |
| |
| private CharSequence transformFromCharacter() { |
| if (arg == null) { |
| return padding("null", 0); |
| } |
| if (arg instanceof Character) { |
| return padding(String.valueOf(arg), 0); |
| } else if (arg instanceof Byte || arg instanceof Short || arg instanceof Integer) { |
| int codePoint = ((Number) arg).intValue(); |
| if (!Character.isValidCodePoint(codePoint)) { |
| throw new IllegalFormatCodePointException(codePoint); |
| } |
| CharSequence result = (codePoint < Character.MIN_SUPPLEMENTARY_CODE_POINT) |
| ? String.valueOf((char) codePoint) |
| : String.valueOf(Character.toChars(codePoint)); |
| return padding(result, 0); |
| } else { |
| throw badArgumentType(); |
| } |
| } |
| |
| private CharSequence transformFromPercent() { |
| return padding("%", 0); |
| } |
| |
| private CharSequence padding(CharSequence source, int startIndex) { |
| int start = startIndex; |
| int width = formatToken.getWidth(); |
| int precision = formatToken.getPrecision(); |
| |
| int length = source.length(); |
| if (precision >= 0) { |
| length = Math.min(length, precision); |
| if (source instanceof StringBuilder) { |
| ((StringBuilder) source).setLength(length); |
| } else { |
| source = source.subSequence(0, length); |
| } |
| } |
| if (width > 0) { |
| width = Math.max(source.length(), width); |
| } |
| if (length >= width) { |
| return source; |
| } |
| |
| char paddingChar = '\u0020'; // space as padding char. |
| if (formatToken.flagZero) { |
| if (formatToken.getConversionType() == 'd') { |
| paddingChar = localeData.zeroDigit; |
| } else { |
| paddingChar = '0'; // No localized digits for bases other than decimal. |
| } |
| } else { |
| // if padding char is space, always pad from the start. |
| start = 0; |
| } |
| char[] paddingChars = new char[width - length]; |
| Arrays.fill(paddingChars, paddingChar); |
| |
| boolean paddingRight = formatToken.flagMinus; |
| StringBuilder result = toStringBuilder(source); |
| if (paddingRight) { |
| result.append(paddingChars); |
| } else { |
| result.insert(start, paddingChars); |
| } |
| return result; |
| } |
| |
| private StringBuilder toStringBuilder(CharSequence cs) { |
| return cs instanceof StringBuilder ? (StringBuilder) cs : new StringBuilder(cs); |
| } |
| |
| private StringBuilder wrapParentheses(StringBuilder result) { |
| result.setCharAt(0, '('); // Replace the '-'. |
| if (formatToken.flagZero) { |
| formatToken.setWidth(formatToken.getWidth() - 1); |
| result = (StringBuilder) padding(result, 1); |
| result.append(')'); |
| } else { |
| result.append(')'); |
| result = (StringBuilder) padding(result, 0); |
| } |
| return result; |
| } |
| |
| private CharSequence transformFromInteger() { |
| int startIndex = 0; |
| StringBuilder result = new StringBuilder(); |
| char currentConversionType = formatToken.getConversionType(); |
| |
| long value; |
| if (arg instanceof Long) { |
| value = ((Long) arg).longValue(); |
| } else if (arg instanceof Integer) { |
| value = ((Integer) arg).longValue(); |
| } else if (arg instanceof Short) { |
| value = ((Short) arg).longValue(); |
| } else if (arg instanceof Byte) { |
| value = ((Byte) arg).longValue(); |
| } else { |
| throw badArgumentType(); |
| } |
| |
| if (formatToken.flagSharp) { |
| if (currentConversionType == 'o') { |
| result.append("0"); |
| startIndex += 1; |
| } else { |
| result.append("0x"); |
| startIndex += 2; |
| } |
| } |
| |
| if (currentConversionType == 'd') { |
| CharSequence digits = Long.toString(value); |
| if (formatToken.flagComma) { |
| digits = insertGrouping(digits); |
| } |
| if (localeData.zeroDigit != '0') { |
| digits = localizeDigits(digits); |
| } |
| result.append(digits); |
| |
| if (value < 0) { |
| if (formatToken.flagParenthesis) { |
| return wrapParentheses(result); |
| } else if (formatToken.flagZero) { |
| startIndex++; |
| } |
| } else { |
| if (formatToken.flagPlus) { |
| result.insert(0, '+'); |
| startIndex += 1; |
| } else if (formatToken.flagSpace) { |
| result.insert(0, ' '); |
| startIndex += 1; |
| } |
| } |
| } else { |
| // Undo sign-extension, since we'll be using Long.to(Octal|Hex)String. |
| if (arg instanceof Byte) { |
| value &= 0xffL; |
| } else if (arg instanceof Short) { |
| value &= 0xffffL; |
| } else if (arg instanceof Integer) { |
| value &= 0xffffffffL; |
| } |
| if (currentConversionType == 'o') { |
| result.append(Long.toOctalString(value)); |
| } else { |
| result.append(Long.toHexString(value)); |
| } |
| } |
| |
| return padding(result, startIndex); |
| } |
| |
| private CharSequence transformFromNull() { |
| formatToken.flagZero = false; |
| return padding("null", 0); |
| } |
| |
| private CharSequence transformFromBigInteger() { |
| int startIndex = 0; |
| StringBuilder result = new StringBuilder(); |
| BigInteger bigInt = (BigInteger) arg; |
| char currentConversionType = formatToken.getConversionType(); |
| |
| if (bigInt == null) { |
| return transformFromNull(); |
| } |
| |
| boolean isNegative = (bigInt.compareTo(BigInteger.ZERO) < 0); |
| |
| if (currentConversionType == 'd') { |
| CharSequence digits = bigInt.toString(10); |
| if (formatToken.flagComma) { |
| digits = insertGrouping(digits); |
| } |
| result.append(digits); |
| } else if (currentConversionType == 'o') { |
| // convert BigInteger to a string presentation using radix 8 |
| result.append(bigInt.toString(8)); |
| } else { |
| // convert BigInteger to a string presentation using radix 16 |
| result.append(bigInt.toString(16)); |
| } |
| if (formatToken.flagSharp) { |
| startIndex = isNegative ? 1 : 0; |
| if (currentConversionType == 'o') { |
| result.insert(startIndex, "0"); |
| startIndex += 1; |
| } else if (currentConversionType == 'x' || currentConversionType == 'X') { |
| result.insert(startIndex, "0x"); |
| startIndex += 2; |
| } |
| } |
| |
| if (!isNegative) { |
| if (formatToken.flagPlus) { |
| result.insert(0, '+'); |
| startIndex += 1; |
| } |
| if (formatToken.flagSpace) { |
| result.insert(0, ' '); |
| startIndex += 1; |
| } |
| } |
| |
| /* pad paddingChar to the output */ |
| if (isNegative && formatToken.flagParenthesis) { |
| return wrapParentheses(result); |
| } |
| if (isNegative && formatToken.flagZero) { |
| startIndex++; |
| } |
| return padding(result, startIndex); |
| } |
| |
| private CharSequence transformFromDateTime() { |
| if (arg == null) { |
| return transformFromNull(); |
| } |
| |
| Calendar calendar; |
| if (arg instanceof Calendar) { |
| calendar = (Calendar) arg; |
| } else { |
| Date date = null; |
| if (arg instanceof Long) { |
| date = new Date(((Long) arg).longValue()); |
| } else if (arg instanceof Date) { |
| date = (Date) arg; |
| } else { |
| throw badArgumentType(); |
| } |
| calendar = Calendar.getInstance(locale); |
| calendar.setTime(date); |
| } |
| |
| StringBuilder result = new StringBuilder(); |
| if (!appendT(result, formatToken.getDateSuffix(), calendar)) { |
| throw formatToken.unknownFormatConversionException(); |
| } |
| return padding(result, 0); |
| } |
| |
| private boolean appendT(StringBuilder result, char conversion, Calendar calendar) { |
| switch (conversion) { |
| case 'A': |
| result.append(localeData.longWeekdayNames[calendar.get(Calendar.DAY_OF_WEEK)]); |
| return true; |
| case 'a': |
| result.append(localeData.shortWeekdayNames[calendar.get(Calendar.DAY_OF_WEEK)]); |
| return true; |
| case 'B': |
| result.append(localeData.longMonthNames[calendar.get(Calendar.MONTH)]); |
| return true; |
| case 'b': case 'h': |
| result.append(localeData.shortMonthNames[calendar.get(Calendar.MONTH)]); |
| return true; |
| case 'C': |
| appendLocalized(result, calendar.get(Calendar.YEAR) / 100, 2); |
| return true; |
| case 'D': |
| appendT(result, 'm', calendar); |
| result.append('/'); |
| appendT(result, 'd', calendar); |
| result.append('/'); |
| appendT(result, 'y', calendar); |
| return true; |
| case 'F': |
| appendT(result, 'Y', calendar); |
| result.append('-'); |
| appendT(result, 'm', calendar); |
| result.append('-'); |
| appendT(result, 'd', calendar); |
| return true; |
| case 'H': |
| appendLocalized(result, calendar.get(Calendar.HOUR_OF_DAY), 2); |
| return true; |
| case 'I': |
| appendLocalized(result, to12Hour(calendar.get(Calendar.HOUR)), 2); |
| return true; |
| case 'L': |
| appendLocalized(result, calendar.get(Calendar.MILLISECOND), 3); |
| return true; |
| case 'M': |
| appendLocalized(result, calendar.get(Calendar.MINUTE), 2); |
| return true; |
| case 'N': |
| appendLocalized(result, calendar.get(Calendar.MILLISECOND) * 1000000L, 9); |
| return true; |
| case 'Q': |
| appendLocalized(result, calendar.getTimeInMillis(), 0); |
| return true; |
| case 'R': |
| appendT(result, 'H', calendar); |
| result.append(':'); |
| appendT(result, 'M', calendar); |
| return true; |
| case 'S': |
| appendLocalized(result, calendar.get(Calendar.SECOND), 2); |
| return true; |
| case 'T': |
| appendT(result, 'H', calendar); |
| result.append(':'); |
| appendT(result, 'M', calendar); |
| result.append(':'); |
| appendT(result, 'S', calendar); |
| return true; |
| case 'Y': |
| appendLocalized(result, calendar.get(Calendar.YEAR), 4); |
| return true; |
| case 'Z': |
| TimeZone timeZone = calendar.getTimeZone(); |
| result.append(timeZone.getDisplayName(timeZone.inDaylightTime(calendar.getTime()), |
| TimeZone.SHORT, locale)); |
| return true; |
| case 'c': |
| appendT(result, 'a', calendar); |
| result.append(' '); |
| appendT(result, 'b', calendar); |
| result.append(' '); |
| appendT(result, 'd', calendar); |
| result.append(' '); |
| appendT(result, 'T', calendar); |
| result.append(' '); |
| appendT(result, 'Z', calendar); |
| result.append(' '); |
| appendT(result, 'Y', calendar); |
| return true; |
| case 'd': |
| appendLocalized(result, calendar.get(Calendar.DAY_OF_MONTH), 2); |
| return true; |
| case 'e': |
| appendLocalized(result, calendar.get(Calendar.DAY_OF_MONTH), 0); |
| return true; |
| case 'j': |
| appendLocalized(result, calendar.get(Calendar.DAY_OF_YEAR), 3); |
| return true; |
| case 'k': |
| appendLocalized(result, calendar.get(Calendar.HOUR_OF_DAY), 0); |
| return true; |
| case 'l': |
| appendLocalized(result, to12Hour(calendar.get(Calendar.HOUR)), 0); |
| return true; |
| case 'm': |
| // Calendar.JANUARY is 0; humans want January represented as 1. |
| appendLocalized(result, calendar.get(Calendar.MONTH) + 1, 2); |
| return true; |
| case 'p': |
| result.append(localeData.amPm[calendar.get(Calendar.AM_PM)].toLowerCase(locale)); |
| return true; |
| case 'r': |
| appendT(result, 'I', calendar); |
| result.append(':'); |
| appendT(result, 'M', calendar); |
| result.append(':'); |
| appendT(result, 'S', calendar); |
| result.append(' '); |
| result.append(localeData.amPm[calendar.get(Calendar.AM_PM)]); |
| return true; |
| case 's': |
| appendLocalized(result, calendar.getTimeInMillis() / 1000, 0); |
| return true; |
| case 'y': |
| appendLocalized(result, calendar.get(Calendar.YEAR) % 100, 2); |
| return true; |
| case 'z': |
| long offset = calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET); |
| char sign = '+'; |
| if (offset < 0) { |
| sign = '-'; |
| offset = -offset; |
| } |
| result.append(sign); |
| appendLocalized(result, offset / 3600000, 2); |
| appendLocalized(result, (offset % 3600000) / 60000, 2); |
| return true; |
| } |
| return false; |
| } |
| |
| private int to12Hour(int hour) { |
| return hour == 0 ? 12 : hour; |
| } |
| |
| private void appendLocalized(StringBuilder result, long value, int width) { |
| int paddingIndex = result.length(); |
| char zeroDigit = localeData.zeroDigit; |
| if (zeroDigit == '0') { |
| result.append(value); |
| } else { |
| result.append(localizeDigits(Long.toString(value))); |
| } |
| int zeroCount = width - (result.length() - paddingIndex); |
| if (zeroCount <= 0) { |
| return; |
| } |
| if (zeroDigit == '0') { |
| result.insert(paddingIndex, ZEROS, 0, zeroCount); |
| } else { |
| for (int i = 0; i < zeroCount; ++i) { |
| result.insert(paddingIndex, zeroDigit); |
| } |
| } |
| } |
| |
| private CharSequence transformFromSpecialNumber(double d) { |
| String source = null; |
| if (Double.isNaN(d)) { |
| source = "NaN"; |
| } else if (d == Double.POSITIVE_INFINITY) { |
| if (formatToken.flagPlus) { |
| source = "+Infinity"; |
| } else if (formatToken.flagSpace) { |
| source = " Infinity"; |
| } else { |
| source = "Infinity"; |
| } |
| } else if (d == Double.NEGATIVE_INFINITY) { |
| if (formatToken.flagParenthesis) { |
| source = "(Infinity)"; |
| } else { |
| source = "-Infinity"; |
| } |
| } else { |
| return null; |
| } |
| |
| formatToken.setPrecision(FormatToken.UNSET); |
| formatToken.flagZero = false; |
| return padding(source, 0); |
| } |
| |
| private CharSequence transformFromFloat() { |
| if (arg == null) { |
| return transformFromNull(); |
| } else if (arg instanceof Float || arg instanceof Double) { |
| Number number = (Number) arg; |
| double d = number.doubleValue(); |
| if (d != d || d == Double.POSITIVE_INFINITY || d == Double.NEGATIVE_INFINITY) { |
| return transformFromSpecialNumber(d); |
| } |
| } else if (arg instanceof BigDecimal) { |
| // BigDecimal can't represent NaN or infinities, but its doubleValue method will return |
| // infinities if the BigDecimal is too big for a double. |
| } else { |
| throw badArgumentType(); |
| } |
| |
| char conversionType = formatToken.getConversionType(); |
| if (conversionType != 'a' && conversionType != 'A' && !formatToken.isPrecisionSet()) { |
| formatToken.setPrecision(FormatToken.DEFAULT_PRECISION); |
| } |
| |
| StringBuilder result = new StringBuilder(); |
| switch (conversionType) { |
| case 'a': case 'A': |
| transformA(result); |
| break; |
| case 'e': case 'E': |
| transformE(result); |
| break; |
| case 'f': |
| transformF(result); |
| break; |
| case 'g': |
| case 'G': |
| transformG(result); |
| break; |
| default: |
| throw formatToken.unknownFormatConversionException(); |
| } |
| |
| formatToken.setPrecision(FormatToken.UNSET); |
| |
| int startIndex = 0; |
| if (result.charAt(0) == localeData.minusSign) { |
| if (formatToken.flagParenthesis) { |
| return wrapParentheses(result); |
| } |
| } else { |
| if (formatToken.flagSpace) { |
| result.insert(0, ' '); |
| startIndex++; |
| } |
| if (formatToken.flagPlus) { |
| result.insert(0, '+'); |
| startIndex++; |
| } |
| } |
| |
| char firstChar = result.charAt(0); |
| if (formatToken.flagZero && (firstChar == '+' || firstChar == localeData.minusSign)) { |
| startIndex = 1; |
| } |
| |
| if (conversionType == 'a' || conversionType == 'A') { |
| startIndex += 2; |
| } |
| return padding(result, startIndex); |
| } |
| |
| private void transformE(StringBuilder result) { |
| // All zeros in this method are *pattern* characters, so no localization. |
| final int precision = formatToken.getPrecision(); |
| String pattern = "0E+00"; |
| if (precision > 0) { |
| StringBuilder sb = new StringBuilder("0."); |
| char[] zeros = new char[precision]; |
| Arrays.fill(zeros, '0'); |
| sb.append(zeros); |
| sb.append("E+00"); |
| pattern = sb.toString(); |
| } |
| |
| NativeDecimalFormat nf = getDecimalFormat(pattern); |
| char[] chars; |
| if (arg instanceof BigDecimal) { |
| chars = nf.formatBigDecimal((BigDecimal) arg, null); |
| } else { |
| chars = nf.formatDouble(((Number) arg).doubleValue(), null); |
| } |
| // Unlike %f, %e uses 'e' (regardless of what the DecimalFormatSymbols would have us use). |
| for (int i = 0; i < chars.length; ++i) { |
| if (chars[i] == 'E') { |
| chars[i] = 'e'; |
| } |
| } |
| result.append(chars); |
| // The # flag requires that we always output a decimal separator. |
| if (formatToken.flagSharp && precision == 0) { |
| int indexOfE = result.indexOf("e"); |
| result.insert(indexOfE, localeData.decimalSeparator); |
| } |
| } |
| |
| private void transformG(StringBuilder result) { |
| int precision = formatToken.getPrecision(); |
| if (precision == 0) { |
| precision = 1; |
| } |
| formatToken.setPrecision(precision); |
| |
| double d = ((Number) arg).doubleValue(); |
| if (d == 0.0) { |
| precision--; |
| formatToken.setPrecision(precision); |
| transformF(result); |
| return; |
| } |
| |
| boolean requireScientificRepresentation = true; |
| d = Math.abs(d); |
| if (Double.isInfinite(d)) { |
| precision = formatToken.getPrecision(); |
| precision--; |
| formatToken.setPrecision(precision); |
| transformE(result); |
| return; |
| } |
| BigDecimal b = new BigDecimal(d, new MathContext(precision)); |
| d = b.doubleValue(); |
| long l = b.longValue(); |
| |
| if (d >= 1 && d < Math.pow(10, precision)) { |
| if (l < Math.pow(10, precision)) { |
| requireScientificRepresentation = false; |
| precision -= String.valueOf(l).length(); |
| precision = precision < 0 ? 0 : precision; |
| l = Math.round(d * Math.pow(10, precision + 1)); |
| if (String.valueOf(l).length() <= formatToken.getPrecision()) { |
| precision++; |
| } |
| formatToken.setPrecision(precision); |
| } |
| } else { |
| l = b.movePointRight(4).longValue(); |
| if (d >= Math.pow(10, -4) && d < 1) { |
| requireScientificRepresentation = false; |
| precision += 4 - String.valueOf(l).length(); |
| l = b.movePointRight(precision + 1).longValue(); |
| if (String.valueOf(l).length() <= formatToken.getPrecision()) { |
| precision++; |
| } |
| l = b.movePointRight(precision).longValue(); |
| if (l >= Math.pow(10, precision - 4)) { |
| formatToken.setPrecision(precision); |
| } |
| } |
| } |
| if (requireScientificRepresentation) { |
| precision = formatToken.getPrecision(); |
| precision--; |
| formatToken.setPrecision(precision); |
| transformE(result); |
| } else { |
| transformF(result); |
| } |
| } |
| |
| private void transformF(StringBuilder result) { |
| // All zeros in this method are *pattern* characters, so no localization. |
| String pattern = "0.000000"; |
| final int precision = formatToken.getPrecision(); |
| if (formatToken.flagComma || precision != FormatToken.DEFAULT_PRECISION) { |
| StringBuilder patternBuilder = new StringBuilder(); |
| if (formatToken.flagComma) { |
| patternBuilder.append(','); |
| int groupingSize = 3; |
| char[] sharps = new char[groupingSize - 1]; |
| Arrays.fill(sharps, '#'); |
| patternBuilder.append(sharps); |
| } |
| patternBuilder.append('0'); |
| if (precision > 0) { |
| patternBuilder.append('.'); |
| for (int i = 0; i < precision; ++i) { |
| patternBuilder.append('0'); |
| } |
| } |
| pattern = patternBuilder.toString(); |
| } |
| |
| NativeDecimalFormat nf = getDecimalFormat(pattern); |
| if (arg instanceof BigDecimal) { |
| result.append(nf.formatBigDecimal((BigDecimal) arg, null)); |
| } else { |
| result.append(nf.formatDouble(((Number) arg).doubleValue(), null)); |
| } |
| // The # flag requires that we always output a decimal separator. |
| if (formatToken.flagSharp && precision == 0) { |
| result.append(localeData.decimalSeparator); |
| } |
| } |
| |
| private void transformA(StringBuilder result) { |
| if (arg instanceof Float) { |
| result.append(Float.toHexString(((Float) arg).floatValue())); |
| } else if (arg instanceof Double) { |
| result.append(Double.toHexString(((Double) arg).doubleValue())); |
| } else { |
| throw badArgumentType(); |
| } |
| |
| if (!formatToken.isPrecisionSet()) { |
| return; |
| } |
| |
| int precision = formatToken.getPrecision(); |
| if (precision == 0) { |
| precision = 1; |
| } |
| int indexOfFirstFractionalDigit = result.indexOf(".") + 1; |
| int indexOfP = result.indexOf("p"); |
| int fractionalLength = indexOfP - indexOfFirstFractionalDigit; |
| |
| if (fractionalLength == precision) { |
| return; |
| } |
| |
| if (fractionalLength < precision) { |
| char[] zeros = new char[precision - fractionalLength]; |
| Arrays.fill(zeros, '0'); // %a shouldn't be localized. |
| result.insert(indexOfP, zeros); |
| return; |
| } |
| result.delete(indexOfFirstFractionalDigit + precision, indexOfP); |
| } |
| |
| private static class FormatSpecifierParser { |
| private String format; |
| private int length; |
| |
| private int startIndex; |
| private int i; |
| |
| /** |
| * Constructs a new parser for the given format string. |
| */ |
| FormatSpecifierParser(String format) { |
| this.format = format; |
| this.length = format.length(); |
| } |
| |
| /** |
| * Returns a FormatToken representing the format specifier starting at 'offset'. |
| * @param offset the first character after the '%' |
| */ |
| FormatToken parseFormatToken(int offset) { |
| this.startIndex = offset; |
| this.i = offset; |
| return parseArgumentIndexAndFlags(new FormatToken()); |
| } |
| |
| /** |
| * Returns a string corresponding to the last format specifier that was parsed. |
| * Used to construct error messages. |
| */ |
| String getFormatSpecifierText() { |
| return format.substring(startIndex, i); |
| } |
| |
| private int peek() { |
| return (i < length) ? format.charAt(i) : -1; |
| } |
| |
| private char advance() { |
| if (i >= length) { |
| throw unknownFormatConversionException(); |
| } |
| return format.charAt(i++); |
| } |
| |
| private UnknownFormatConversionException unknownFormatConversionException() { |
| throw new UnknownFormatConversionException(getFormatSpecifierText()); |
| } |
| |
| private FormatToken parseArgumentIndexAndFlags(FormatToken token) { |
| // Parse the argument index, if there is one. |
| int position = i; |
| int ch = peek(); |
| if (Character.isDigit(ch)) { |
| int number = nextInt(); |
| if (peek() == '$') { |
| // The number was an argument index. |
| advance(); // Swallow the '$'. |
| if (number == FormatToken.UNSET) { |
| throw new MissingFormatArgumentException(getFormatSpecifierText()); |
| } |
| // k$ stands for the argument whose index is k-1 except that |
| // 0$ and 1$ both stand for the first element. |
| token.setArgIndex(Math.max(0, number - 1)); |
| } else { |
| if (ch == '0') { |
| // The digit zero is a format flag, so reparse it as such. |
| i = position; |
| } else { |
| // The number was a width. This means there are no flags to parse. |
| return parseWidth(token, number); |
| } |
| } |
| } else if (ch == '<') { |
| token.setArgIndex(FormatToken.LAST_ARGUMENT_INDEX); |
| advance(); |
| } |
| |
| // Parse the flags. |
| while (token.setFlag(peek())) { |
| advance(); |
| } |
| |
| // What comes next? |
| ch = peek(); |
| if (Character.isDigit(ch)) { |
| return parseWidth(token, nextInt()); |
| } else if (ch == '.') { |
| return parsePrecision(token); |
| } else { |
| return parseConversionType(token); |
| } |
| } |
| |
| // We pass the width in because in some cases we've already parsed it. |
| // (Because of the ambiguity between argument indexes and widths.) |
| private FormatToken parseWidth(FormatToken token, int width) { |
| token.setWidth(width); |
| int ch = peek(); |
| if (ch == '.') { |
| return parsePrecision(token); |
| } else { |
| return parseConversionType(token); |
| } |
| } |
| |
| private FormatToken parsePrecision(FormatToken token) { |
| advance(); // Swallow the '.'. |
| int ch = peek(); |
| if (Character.isDigit(ch)) { |
| token.setPrecision(nextInt()); |
| return parseConversionType(token); |
| } else { |
| // The precision is required but not given by the format string. |
| throw unknownFormatConversionException(); |
| } |
| } |
| |
| private FormatToken parseConversionType(FormatToken token) { |
| char conversionType = advance(); // A conversion type is mandatory. |
| token.setConversionType(conversionType); |
| if (conversionType == 't' || conversionType == 'T') { |
| char dateSuffix = advance(); // A date suffix is mandatory for 't' or 'T'. |
| token.setDateSuffix(dateSuffix); |
| } |
| return token; |
| } |
| |
| // Parses an integer (of arbitrary length, but typically just one digit). |
| private int nextInt() { |
| long value = 0; |
| while (i < length && Character.isDigit(format.charAt(i))) { |
| value = 10 * value + (format.charAt(i++) - '0'); |
| if (value > Integer.MAX_VALUE) { |
| return failNextInt(); |
| } |
| } |
| return (int) value; |
| } |
| |
| // Swallow remaining digits to resync our attempted parse, but return failure. |
| private int failNextInt() { |
| while (Character.isDigit(peek())) { |
| advance(); |
| } |
| return FormatToken.UNSET; |
| } |
| } |
| } |