| /* 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.CharBuffer; |
| import java.nio.charset.Charset; |
| import java.security.AccessController; |
| import java.security.PrivilegedAction; |
| import java.text.DateFormatSymbols; |
| import java.text.DecimalFormat; |
| import java.text.DecimalFormatSymbols; |
| import java.text.NumberFormat; |
| |
| /** |
| * <p>The {@code Formatter} class is a String-formatting utility that is designed |
| * to work like the {@code printf} function of the C programming language. |
| * Its key methods are the {@code format} methods which create a formatted |
| * {@code String} by replacing a set of placeholders (format tokens) with formatted |
| * values. The style used to format each value is determined by the format |
| * token used. For example, the call<br/> |
| * {@code format("My decimal value is %d and my String is %s.", 3, "Hello");}<br/> |
| * returns the {@code String}<br/> |
| * {@code My decimal value is 3 and my String is Hello.} |
| * |
| * <p>The format token consists of a percent sign, optionally followed |
| * by flags and precision arguments, and then a single character that |
| * indicates the type of value |
| * being formatted. If the type is a time/date, then the type character |
| * {@code t} is followed by an additional character that indicates how the |
| * date is to be formatted. The two characters {@code <$} immediately |
| * following the % sign indicate that the previous value should be used again |
| * instead of moving on to the next value argument. A number {@code n} |
| * and a dollar sign immediately following the % sign make n the next argument |
| * to be used. |
| * |
| * <p>The available choices are the following: |
| * |
| * <table BORDER="1" WIDTH="100%" CELLPADDING="3" CELLSPACING="0" SUMMARY=""> |
| * <tr BGCOLOR="#CCCCFF" CLASS="TableHeadingColor"> |
| * <TD COLSPAN=4> |
| * <B>Text value types</B></TD> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code s}</td> |
| * <td width="10%">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}, {@code s}</td> |
| * <td width="10%">String to capitals</td> |
| * <td width="30%">{@code format("%S, %S", "hello", "Hello");}</td> |
| * <td width="30%">{@code HELLO, HELLO}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code c}</td> |
| * <td width="10%">Character</td> |
| * <td width="30%">{@code format("%c, %c", 'd', 0x65);}</td> |
| * <td width="30%">{@code d, e}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code C}</td> |
| * <td width="10%">Character to capitals</td> |
| * <td width="30%">{@code format("%C, %C", 'd', 0x65);}</td> |
| * <td width="30%">{@code D, E}</td> |
| * </tr> |
| * <tr BGCOLOR="#CCCCFF" CLASS="TableHeadingColor"> |
| * <TD COLSPAN=4> |
| * <B>Text option flags</B><br/>The value between the |
| * option and the type character indicates the minimum width in |
| * characters of the formatted value </TD> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code -}</td> |
| * <td width="10%">Left justify (width value is required)</td> |
| * <td width="30%">{@code format("%-3C, %3C", 'd', 0x65);}</td> |
| * <td width="30%">{@code D , E}</td> |
| * </tr> |
| * <tr BGCOLOR="#CCCCFF" CLASS="TableHeadingColor"> |
| * <TD COLSPAN=4> |
| * <B>Integer types</B></TD> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code d}</td> |
| * <td width="10%">int, formatted as decimal</td> |
| * <td width="30%">{@code format("%d, %d"1$, 35, 0x10);}</td> |
| * <td width="30%">{@code 35, 16}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code o}</td> |
| * <td width="10%">int, formatted as octal</td> |
| * <td width="30%">{@code format("%o, %o", 8, 010);}</td> |
| * <td width="30%">{@code 10, 10}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code X}, {@code x}</td> |
| * <td width="10%">int, formatted as hexidecimal</td> |
| * <td width="30%">{@code format("%x, %X", 10, 10);}</td> |
| * <td width="30%">{@code a, A}</td> |
| * </tr> |
| * <tr BGCOLOR="#CCCCFF" CLASS="TableHeadingColor"> |
| * <TD COLSPAN=4> |
| * <B>Integer option flags</B><br/>The value between the |
| * option and the type character indicates the minimum width in |
| * characters of the formatted value </TD> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code +}</td> |
| * <td width="10%">lead with the number's sign</td> |
| * <td width="30%">{@code format("%+d, %+4d", 5, 5);}</td> |
| * <td width="30%">{@code +5, +5}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code -}</td> |
| * <td width="10%">Left justify (width value is required)</td> |
| * <td width="30%">{@code format("%-6dx", 5);}</td> |
| * <td width="30%">{@code 5 x}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code #}</td> |
| * <td width="10%">Print the leading characters that indicate |
| * hexidecimal or octal (for use only with hex and octal types) </td> |
| * <td width="30%">{@code format("%#o", 010);}</td> |
| * <td width="30%">{@code 010}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code }</td> |
| * <td width="10%">A space indicates that non-negative numbers |
| * should have a leading space. </td> |
| * <td width="30%">{@code format("x% d% 5d", 4, 4);}</td> |
| * <td width="30%">{@code x 4 4}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code 0}</td> |
| * <td width="10%">Pad the number with leading zeros (width value is required)</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="10%">Put parentheses around negative numbers (decimal only)</td> |
| * <td width="30%">{@code format("%(d, %(d, %(6d", 12, -12, -12);}</td> |
| * <td width="30%">{@code 12, (12), (12)}</td> |
| * </tr> |
| * <tr BGCOLOR="#CCCCFF" CLASS="TableHeadingColor"> |
| * <TD COLSPAN=4> |
| * <B>Float types</B><br/>A value immediately following the % symbol |
| * gives the minimum width in characters of the formatted value; if it |
| * is followed by a period and another integer, then the second value |
| * gives the precision (6 by default).</TD> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code f}</td> |
| * <td width="10%">float (or double) formatted as a decimal, where |
| * the precision indicates the number of digits after the decimal.</td> |
| * <td width="30%">{@code format("%f %<.1f %<1.5f %<10f %<6.0f", 123.456f);}</td> |
| * <td width="30%">{@code 123.456001 123.5 123.45600 123.456001 123}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code E}, {@code e}</td> |
| * <td width="10%">float (or double) formatted in decimal exponential |
| * notation, where the precision indicates the number of significant digits.</td> |
| * <td width="30%">{@code format("%E %<.1e %<1.5E %<10E %<6.0E", 123.456f);}</td> |
| * <td width="30%">{@code 1.234560E+02 1.2e+02 1.23456E+02 1.234560E+02 1E+02}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code G}, {@code g}</td> |
| * <td width="10%">float (or double) formatted in decimal exponential |
| * notation , where the precision indicates the maximum number of significant digits.</td> |
| * <td width="30%">{@code format("%G %<.1g %<1.5G %<10G %<6.0G", 123.456f);}</td> |
| * <td width="30%">{@code 123.456 1e+02 123.46 123.456 1E+02}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code A}, {@code a}</td> |
| * <td width="10%">float (or double) formatted as a hexidecimal in exponential |
| * notation, where the precision indicates the number of significant digits.</td> |
| * <td width="30%">{@code format("%A %<.1a %<1.5A %<10A %<6.0A", 123.456f);}</td> |
| * <td width="30%">{@code 0X1.EDD2F2P6 0x1.fp6 0X1.EDD2FP6 0X1.EDD2F2P6 0X1.FP6}</td> |
| * </tr> |
| * <tr BGCOLOR="#CCCCFF" CLASS="TableHeadingColor"> |
| * <TD COLSPAN=4> |
| * <B>Float-type option flags</B><br/>See the Integer-type options. |
| * The options for float-types are the |
| * same as for integer types with one addition: </TD> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code ,}</td> |
| * <td width="10%">Use a comma in place of a decimal if the locale |
| * requires it. </td> |
| * <td width="30%">{@code format(new Locale("fr"), "%,7.2f", 6.03f);}</td> |
| * <td width="30%">{@code 6,03}</td> |
| * </tr> |
| * <tr BGCOLOR="#CCCCFF" CLASS="TableHeadingColor"> |
| * <TD COLSPAN=4> |
| * <B>Date types</B></TD> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code t}, {@code T}</td> |
| * <td width="10%">Date</td> |
| * <td width="30%">{@code format(new Locale("fr"), "%tB %TB", Calendar.getInstance(), Calendar.getInstance());}</td> |
| * <td width="30%">{@code avril AVRIL}</td> |
| * </tr> |
| * <tr BGCOLOR="#CCCCFF" CLASS="TableHeadingColor"> |
| * <TD COLSPAN=4> |
| * <B>Date format precisions</B><br/>The format precision character |
| * follows the {@code t}. </TD> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code A}, {@code a}</td> |
| * <td width="10%">The day of the week</td> |
| * <td width="30%">{@code format("%ta %tA", cal, cal);}</td> |
| * <td width="30%">{@code Tue Tuesday}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code b}, {@code B}, {@code h}</td> |
| * <td width="10%">The name of the month</td> |
| * <td width="30%">{@code format("%tb %<tB %<th", cal, cal, cal);}</td> |
| * <td width="30%">{@code Apr April Apr}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code C}</td> |
| * <td width="10%">The century</td> |
| * <td width="30%">{@code format("%tC\n", cal);}</td> |
| * <td width="30%">{@code 20}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code d}, {@code e}</td> |
| * <td width="10%">The day of the month (with or without leading zeros)</td> |
| * <td width="30%">{@code format("%td %te", cal, cal);}</td> |
| * <td width="30%">{@code 01 1}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code F}</td> |
| * <td width="10%">The complete date formatted as 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 D}</td> |
| * <td width="10%">The complete date formatted as MM/DD/YY |
| * (not corrected for locale) </td> |
| * <td width="30%">{@code format(new Locale("en_US"), "%tD", cal);<br/>format(new Locale("en_UK"), " %tD", cal);}</td> |
| * <td width="30%">{@code 04/01/08 04/01/08}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code j}</td> |
| * <td width="10%">The number of the day (from the beginning of the year).</td> |
| * <td width="30%">{@code format("%tj\n", cal);}</td> |
| * <td width="30%">{@code 092}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code m}</td> |
| * <td width="10%">The number of the month</td> |
| * <td width="30%">{@code format("%tm\n", cal);}</td> |
| * <td width="30%">{@code 04}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code y}, {@code Y}</td> |
| * <td width="10%">The year</td> |
| * <td width="30%">{@code format("%ty %tY", cal, cal);}</td> |
| * <td width="30%">{@code 08 2008}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code H}, {@code I}, {@code k}, {@code l}</td> |
| * <td width="10%">The hour of the day, in 12 or 24 hour format, with or |
| * without a leading zero</td> |
| * <td width="30%">{@code format("%tH %tI %tk %tl", cal, cal, cal, cal);}</td> |
| * <td width="30%">{@code 16 04 16 4}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code p}</td> |
| * <td width="10%">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 M}, {@code S}, {@code L}, {@code N}</td> |
| * <td width="10%">The minutes, seconds, milliseconds, and nanoseconds</td> |
| * <td width="30%">{@code format("%tM %tS %tL %tN", cal, cal, cal, cal);}</td> |
| * <td width="30%">{@code 08 17 359 359000000}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code Z}, {@code z}</td> |
| * <td width="10%">The time zone: its abbreviation or offset from GMT</td> |
| * <td width="30%">{@code format("%tZ %tz", cal, cal);}</td> |
| * <td width="30%">{@code CEST +0100}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code R}, {@code r}, {@code T}</td> |
| * <td width="10%">The complete time</td> |
| * <td width="30%">{@code format("%tR %tr %tT", cal, cal, cal);}</td> |
| * <td width="30%">{@code 16:15 04:15:32 PM 16:15:32}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code s}, {@code Q}</td> |
| * <td width="10%">The number of seconds or milliseconds from "the epoch" |
| * (1 January 1970 00:00:00 UTC) </td> |
| * <td width="30%">{@code format("%ts %tQ", cal, cal);}</td> |
| * <td width="30%">{@code 1207059412 1207059412656}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code c}</td> |
| * <td width="10%">The complete time and date</td> |
| * <td width="30%">{@code format("%tc", cal);}</td> |
| * <td width="30%">{@code Tue Apr 01 16:19:17 CEST 2008}</td> |
| * </tr> |
| * <tr BGCOLOR="#CCCCFF" CLASS="TableHeadingColor"> |
| * <TD COLSPAN=4> |
| * <B>Other data types</B></TD> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code B}, {@code b}</td> |
| * <td width="10%">Boolean</td> |
| * <td width="30%">{@code format("%b, %B", true, false);}</td> |
| * <td width="30%">{@code true, FALSE}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code H}, {@code h}</td> |
| * <td width="10%">Hashcode</td> |
| * <td width="30%">{@code format("%h, %H", obj, obj);}</td> |
| * <td width="30%">{@code 190d11, 190D11}</td> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code n}</td> |
| * <td width="10%">line separator</td> |
| * <td width="30%">{@code format("first%nsecond", "???");}</td> |
| * <td width="30%">{@code first<br/>second}</td> |
| * </tr> |
| * <tr BGCOLOR="#CCCCFF" CLASS="TableHeadingColor"> |
| * <TD COLSPAN=4> |
| * <B>Escape sequences</B></TD> |
| * </tr> |
| * <tr> |
| * <td width="5%">{@code %}</td> |
| * <td width="10%">Escape the % character</td> |
| * <td width="30%">{@code format("%d%%, %d", 50, 60);}</td> |
| * <td width="30%">{@code 50%, 60}</td> |
| * </tr> |
| * </table> |
| * |
| * <p>An instance of Formatter can be created to write the formatted |
| * output to standard types of output streams. Its functionality can |
| * also be accessed through the format methods of an output stream |
| * or of {@code String}:<br/> |
| * {@code System.out.println(String.format("%ty\n", cal));}<br/> |
| * {@code System.out.format("%ty\n", cal);} |
| * |
| * <p>The class is not multi-threaded safe. The user is responsible for |
| * maintaining a thread-safe design if a {@code Formatter} is |
| * accessed by multiple threads. |
| * |
| * @since 1.5 |
| */ |
| public final class Formatter implements Closeable, Flushable { |
| |
| /** |
| * 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 |
| } |
| |
| private Appendable out; |
| |
| private Locale locale; |
| |
| private boolean closed = false; |
| |
| private IOException lastIOException; |
| |
| /** |
| * Constructs a {@code Formatter}. |
| * |
| * 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()}. |
| * |
| * The {@code Locale} for the {@code Formatter} is the default {@code Locale}. |
| */ |
| public Formatter() { |
| this(new StringBuilder(), Locale.getDefault()); |
| } |
| |
| /** |
| * Constructs a {@code Formatter} whose output will be written to the |
| * specified {@code Appendable}. |
| * |
| * The locale for the {@code Formatter} is the default {@code Locale}. |
| * |
| * @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}. |
| * |
| * 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 (null == a) { |
| out = new StringBuilder(); |
| } else { |
| out = a; |
| } |
| locale = l; |
| } |
| |
| /** |
| * Constructs a {@code Formatter} whose output is written to the specified file. |
| * |
| * The charset of the {@code Formatter} is the default charset. |
| * |
| * The {@code Locale} for the {@code Formatter} is the default {@code Locale}. |
| * |
| * @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. |
| * @throws SecurityException |
| * if there is a {@code SecurityManager} in place which denies permission |
| * to write to the file in {@code checkWrite(file.getPath())}. |
| */ |
| public Formatter(String fileName) throws FileNotFoundException { |
| this(new File(fileName)); |
| |
| } |
| |
| /** |
| * Constructs a {@code Formatter} whose output is written to the specified file. |
| * |
| * The {@code Locale} for the {@code Formatter} is the default {@code Locale}. |
| * |
| * @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 SecurityException |
| * if there is a {@code SecurityManager} in place which denies permission |
| * to write to the file in {@code checkWrite(file.getPath())}. |
| * @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 SecurityException |
| * if there is a {@code SecurityManager} in place which denies permission |
| * to write to the file in {@code checkWrite(file.getPath())}. |
| * @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. |
| * |
| * The {@code Locale} for the {@code Formatter} is the default {@code Locale}. |
| * |
| * @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}. |
| * @throws SecurityException |
| * if there is a {@code SecurityManager} in place which denies permission |
| * to write to the {@code File} in {@code checkWrite(file.getPath())}. |
| */ |
| 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}. |
| * |
| * The {@code Locale} for the {@code Formatter} is the default {@code Locale}. |
| * |
| * @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 SecurityException |
| * if there is a {@code SecurityManager} in place which denies permission |
| * to write to the {@code File} in {@code checkWrite(file.getPath())}. |
| * @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 SecurityException |
| * if there is a {@code SecurityManager} in place which denies permission |
| * to write to the {@code File} in {@code checkWrite(file.getPath())}. |
| * @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); |
| OutputStreamWriter writer = new OutputStreamWriter(fout, csn); |
| // BEGIN android-changed |
| out = new BufferedWriter(writer, 8192); |
| // END android-changed |
| } catch (RuntimeException e) { |
| closeOutputStream(fout); |
| throw e; |
| } catch (UnsupportedEncodingException e) { |
| closeOutputStream(fout); |
| throw e; |
| } |
| |
| locale = l; |
| } |
| |
| /** |
| * Constructs a {@code Formatter} whose output is written to the specified {@code OutputStream}. |
| * |
| * The charset of the {@code Formatter} is the default charset. |
| * |
| * The {@code Locale} for the {@code Formatter} is the default {@code Locale}. |
| * |
| * @param os |
| * the stream to be used as the destination of the {@code Formatter}. |
| */ |
| public Formatter(OutputStream os) { |
| OutputStreamWriter writer = new OutputStreamWriter(os, Charset |
| .defaultCharset()); |
| // BEGIN android-changed |
| out = new BufferedWriter(writer, 8192); |
| // END android-changed |
| locale = Locale.getDefault(); |
| } |
| |
| /** |
| * Constructs a {@code Formatter} with the given charset, |
| * and whose output is written to the specified {@code OutputStream}. |
| * |
| * The {@code Locale} for the {@code Formatter} is the default {@code Locale}. |
| * |
| * @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 { |
| |
| OutputStreamWriter writer = new OutputStreamWriter(os, csn); |
| // BEGIN android-changed |
| out = new BufferedWriter(writer, 8192); |
| // END android-changed |
| |
| locale = l; |
| } |
| |
| /** |
| * Constructs a {@code Formatter} whose output is written to the specified {@code PrintStream}. |
| * |
| * The charset of the {@code Formatter} is the default charset. |
| * |
| * The {@code Locale} for the {@code Formatter} is the default {@code Locale}. |
| * |
| * @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 (null == ps) { |
| throw new NullPointerException(); |
| } |
| out = ps; |
| locale = Locale.getDefault(); |
| } |
| |
| private void checkClosed() { |
| 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() { |
| checkClosed(); |
| 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() { |
| checkClosed(); |
| 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() { |
| checkClosed(); |
| 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() { |
| checkClosed(); |
| 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() { |
| 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(locale, format, args); |
| } |
| |
| // BEGIN android-added |
| /** |
| * Cached transformer. Improves performance when format() is called multiple |
| * times. |
| */ |
| private Transformer transformer; |
| // END android-added |
| |
| /** |
| * 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 influence the {@code Locale} specified during |
| * construction. |
| * @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) { |
| checkClosed(); |
| CharBuffer formatBuffer = CharBuffer.wrap(format); |
| ParserStateMachine parser = new ParserStateMachine(formatBuffer); |
| |
| // BEGIN android-changed |
| // Reuse the previous transformer if the locale matches. |
| if (transformer == null || ! transformer.locale.equals(l)) { |
| transformer = new Transformer(this, l); |
| } |
| // END android-changed |
| |
| int currentObjectIndex = 0; |
| Object lastArgument = null; |
| boolean hasLastArgumentSet = false; |
| while (formatBuffer.hasRemaining()) { |
| parser.reset(); |
| FormatToken token = parser.getNextFormatToken(); |
| String result; |
| String plainText = token.getPlainText(); |
| if (token.getConversionType() == (char) FormatToken.UNSET) { |
| result = plainText; |
| } else { |
| plainText = plainText.substring(0, plainText.indexOf('%')); |
| Object argument = null; |
| if (token.requireArgument()) { |
| int index = token.getArgIndex() == FormatToken.UNSET ? currentObjectIndex++ |
| : token.getArgIndex(); |
| argument = getArgument(args, index, token, lastArgument, |
| hasLastArgumentSet); |
| lastArgument = argument; |
| hasLastArgumentSet = true; |
| } |
| result = transformer.transform(token, argument); |
| result = (null == result ? plainText : plainText + result); |
| } |
| // if output is made by formattable callback |
| if (null != result) { |
| try { |
| out.append(result); |
| } catch (IOException e) { |
| lastIOException = e; |
| } |
| } |
| } |
| return this; |
| } |
| |
| private Object getArgument(Object[] args, int index, FormatToken token, |
| Object lastArgument, boolean hasLastArgumentSet) { |
| if (index == FormatToken.LAST_ARGUMENT_INDEX && !hasLastArgumentSet) { |
| throw new MissingFormatArgumentException("<"); //$NON-NLS-1$ |
| } |
| |
| if (null == args) { |
| return null; |
| } |
| |
| if (index >= args.length) { |
| throw new MissingFormatArgumentException(token.getPlainText()); |
| } |
| |
| if (index == FormatToken.LAST_ARGUMENT_INDEX) { |
| return lastArgument; |
| } |
| |
| return args[index]; |
| } |
| |
| private static void closeOutputStream(OutputStream os) { |
| if (null == os) { |
| return; |
| } |
| try { |
| os.close(); |
| |
| } catch (IOException e) { |
| // silently |
| } |
| } |
| |
| /* |
| * Information about the format string of a specified argument, which |
| * includes the conversion type, flags, width, precision and the argument |
| * index as well as the plainText that contains the whole format string used |
| * as the result for output if necessary. Besides, the string for flags is |
| * recorded to construct corresponding FormatExceptions if necessary. |
| */ |
| 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_MINUS = 1; |
| |
| static final int FLAG_SHARP = 1 << 1; |
| |
| static final int FLAG_ADD = 1 << 2; |
| |
| static final int FLAG_SPACE = 1 << 3; |
| |
| static final int FLAG_ZERO = 1 << 4; |
| |
| static final int FLAG_COMMA = 1 << 5; |
| |
| static final int FLAG_PARENTHESIS = 1 << 6; |
| |
| private static final int FLAGT_TYPE_COUNT = 6; |
| |
| private int formatStringStartIndex; |
| |
| private String plainText; |
| |
| private int argIndex = UNSET; |
| |
| private int flags = 0; |
| |
| private int width = UNSET; |
| |
| private int precision = UNSET; |
| |
| private StringBuilder strFlags = new StringBuilder(FLAGT_TYPE_COUNT); |
| |
| private char dateSuffix;// will be used in new feature. |
| |
| private char conversionType = (char) UNSET; |
| |
| boolean isPrecisionSet() { |
| return precision != UNSET; |
| } |
| |
| boolean isWidthSet() { |
| return width != UNSET; |
| } |
| |
| boolean isFlagSet(int flag) { |
| return 0 != (flags & flag); |
| } |
| |
| int getArgIndex() { |
| return argIndex; |
| } |
| |
| void setArgIndex(int index) { |
| argIndex = index; |
| } |
| |
| String getPlainText() { |
| return plainText; |
| } |
| |
| void setPlainText(String plainText) { |
| this.plainText = plainText; |
| } |
| |
| 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.toString(); |
| } |
| |
| int getFlags() { |
| return flags; |
| } |
| |
| void setFlags(int flags) { |
| this.flags = flags; |
| } |
| |
| /* |
| * 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(char c) { |
| int newFlag; |
| switch (c) { |
| case '-': { |
| newFlag = FLAG_MINUS; |
| break; |
| } |
| case '#': { |
| newFlag = FLAG_SHARP; |
| break; |
| } |
| case '+': { |
| newFlag = FLAG_ADD; |
| break; |
| } |
| case ' ': { |
| newFlag = FLAG_SPACE; |
| break; |
| } |
| case '0': { |
| newFlag = FLAG_ZERO; |
| break; |
| } |
| case ',': { |
| newFlag = FLAG_COMMA; |
| break; |
| } |
| case '(': { |
| newFlag = FLAG_PARENTHESIS; |
| break; |
| } |
| default: |
| return false; |
| } |
| if (0 != (flags & newFlag)) { |
| throw new DuplicateFormatFlagsException(String.valueOf(c)); |
| } |
| flags = (flags | newFlag); |
| strFlags.append(c); |
| return true; |
| |
| } |
| |
| int getFormatStringStartIndex() { |
| return formatStringStartIndex; |
| } |
| |
| void setFormatStringStartIndex(int index) { |
| formatStringStartIndex = index; |
| } |
| |
| 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'; |
| } |
| } |
| |
| /* |
| * Transforms the argument to the formatted string according to the format |
| * information contained in the format token. |
| */ |
| private static class Transformer { |
| |
| private Formatter formatter; |
| |
| private FormatToken formatToken; |
| |
| private Object arg; |
| |
| private Locale locale; |
| |
| private static String lineSeparator; |
| |
| private NumberFormat numberFormat; |
| |
| private DecimalFormatSymbols decimalFormatSymbols; |
| |
| private DateTimeUtil dateTimeUtil; |
| |
| Transformer(Formatter formatter, Locale locale) { |
| this.formatter = formatter; |
| this.locale = (null == locale ? Locale.US : locale); |
| } |
| |
| private NumberFormat getNumberFormat() { |
| if (null == numberFormat) { |
| numberFormat = NumberFormat.getInstance(locale); |
| } |
| return numberFormat; |
| } |
| |
| private DecimalFormatSymbols getDecimalFormatSymbols() { |
| if (null == decimalFormatSymbols) { |
| decimalFormatSymbols = new DecimalFormatSymbols(locale); |
| } |
| return decimalFormatSymbols; |
| } |
| |
| /* |
| * Gets the formatted string according to the format token and the |
| * argument. |
| */ |
| String transform(FormatToken token, Object argument) { |
| |
| /* init data member to print */ |
| this.formatToken = token; |
| this.arg = argument; |
| |
| String 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 (null == arg || arg instanceof BigInteger) { |
| result = transformFromBigInteger(); |
| } else { |
| result = transformFromInteger(); |
| } |
| break; |
| } |
| case 'e': |
| case 'E': |
| case 'g': |
| case 'G': |
| case 'f': |
| case 'a': |
| case 'A': { |
| result = transformFromFloat(); |
| break; |
| } |
| case '%': { |
| result = transformFromPercent(); |
| break; |
| } |
| case 'n': { |
| result = transformFromLineSeparator(); |
| break; |
| } |
| case 't': |
| case 'T': { |
| result = transformFromDateTime(); |
| break; |
| } |
| default: { |
| throw new UnknownFormatConversionException(String |
| .valueOf(token.getConversionType())); |
| } |
| } |
| |
| if (Character.isUpperCase(token.getConversionType())) { |
| if (null != result) { |
| result = result.toUpperCase(Locale.US); |
| } |
| } |
| return result; |
| } |
| |
| /* |
| * Transforms the Boolean argument to a formatted string. |
| */ |
| private String transformFromBoolean() { |
| StringBuilder result = new StringBuilder(); |
| int startIndex = 0; |
| int flags = formatToken.getFlags(); |
| |
| if (formatToken.isFlagSet(FormatToken.FLAG_MINUS) |
| && !formatToken.isWidthSet()) { |
| throw new MissingFormatWidthException("-" //$NON-NLS-1$ |
| + formatToken.getConversionType()); |
| } |
| |
| // only '-' is valid for flags |
| if (FormatToken.FLAGS_UNSET != flags |
| && FormatToken.FLAG_MINUS != flags) { |
| throw new FormatFlagsConversionMismatchException(formatToken |
| .getStrFlags(), formatToken.getConversionType()); |
| } |
| |
| if (null == arg) { |
| result.append("false"); //$NON-NLS-1$ |
| } else if (arg instanceof Boolean) { |
| result.append(arg); |
| } else { |
| result.append("true"); //$NON-NLS-1$ |
| } |
| return padding(result, startIndex); |
| } |
| |
| /* |
| * Transforms the hashcode of the argument to a formatted string. |
| */ |
| private String transformFromHashCode() { |
| StringBuilder result = new StringBuilder(); |
| |
| int startIndex = 0; |
| int flags = formatToken.getFlags(); |
| |
| if (formatToken.isFlagSet(FormatToken.FLAG_MINUS) |
| && !formatToken.isWidthSet()) { |
| throw new MissingFormatWidthException("-" //$NON-NLS-1$ |
| + formatToken.getConversionType()); |
| } |
| |
| // only '-' is valid for flags |
| if (FormatToken.FLAGS_UNSET != flags |
| && FormatToken.FLAG_MINUS != flags) { |
| throw new FormatFlagsConversionMismatchException(formatToken |
| .getStrFlags(), formatToken.getConversionType()); |
| } |
| |
| if (null == arg) { |
| result.append("null"); //$NON-NLS-1$ |
| } else { |
| result.append(Integer.toHexString(arg.hashCode())); |
| } |
| return padding(result, startIndex); |
| } |
| |
| /* |
| * Transforms the String to a formatted string. |
| */ |
| private String transformFromString() { |
| StringBuilder result = new StringBuilder(); |
| int startIndex = 0; |
| int flags = formatToken.getFlags(); |
| |
| if (formatToken.isFlagSet(FormatToken.FLAG_MINUS) |
| && !formatToken.isWidthSet()) { |
| throw new MissingFormatWidthException("-" //$NON-NLS-1$ |
| + formatToken.getConversionType()); |
| } |
| |
| if (arg instanceof Formattable) { |
| int flag = 0; |
| // only minus and sharp flag is valid |
| if (FormatToken.FLAGS_UNSET != (flags & ~FormatToken.FLAG_MINUS & ~FormatToken.FLAG_SHARP)) { |
| throw new IllegalFormatFlagsException(formatToken |
| .getStrFlags()); |
| } |
| if (formatToken.isFlagSet(FormatToken.FLAG_MINUS)) { |
| flag |= FormattableFlags.LEFT_JUSTIFY; |
| } |
| if (formatToken.isFlagSet(FormatToken.FLAG_SHARP)) { |
| flag |= FormattableFlags.ALTERNATE; |
| } |
| if (Character.isUpperCase(formatToken.getConversionType())) { |
| flag |= FormattableFlags.UPPERCASE; |
| } |
| ((Formattable) arg).formatTo(formatter, flag, 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; |
| } |
| // only '-' is valid for flags if the argument is not an |
| // instance of Formattable |
| if (FormatToken.FLAGS_UNSET != flags |
| && FormatToken.FLAG_MINUS != flags) { |
| throw new FormatFlagsConversionMismatchException(formatToken |
| .getStrFlags(), formatToken.getConversionType()); |
| } |
| |
| result.append(arg); |
| return padding(result, startIndex); |
| } |
| |
| /* |
| * Transforms the Character to a formatted string. |
| */ |
| private String transformFromCharacter() { |
| StringBuilder result = new StringBuilder(); |
| |
| int startIndex = 0; |
| int flags = formatToken.getFlags(); |
| |
| if (formatToken.isFlagSet(FormatToken.FLAG_MINUS) |
| && !formatToken.isWidthSet()) { |
| throw new MissingFormatWidthException("-" //$NON-NLS-1$ |
| + formatToken.getConversionType()); |
| } |
| |
| // only '-' is valid for flags |
| if (FormatToken.FLAGS_UNSET != flags |
| && FormatToken.FLAG_MINUS != flags) { |
| throw new FormatFlagsConversionMismatchException(formatToken |
| .getStrFlags(), formatToken.getConversionType()); |
| } |
| |
| if (formatToken.isPrecisionSet()) { |
| throw new IllegalFormatPrecisionException(formatToken |
| .getPrecision()); |
| } |
| |
| if (null == arg) { |
| result.append("null"); //$NON-NLS-1$ |
| } else { |
| if (arg instanceof Character) { |
| result.append(arg); |
| } else if (arg instanceof Byte) { |
| byte b = ((Byte) arg).byteValue(); |
| if (!Character.isValidCodePoint(b)) { |
| throw new IllegalFormatCodePointException(b); |
| } |
| result.append((char) b); |
| } else if (arg instanceof Short) { |
| short s = ((Short) arg).shortValue(); |
| if (!Character.isValidCodePoint(s)) { |
| throw new IllegalFormatCodePointException(s); |
| } |
| result.append((char) s); |
| } else if (arg instanceof Integer) { |
| int codePoint = ((Integer) arg).intValue(); |
| if (!Character.isValidCodePoint(codePoint)) { |
| throw new IllegalFormatCodePointException(codePoint); |
| } |
| result.append(String.valueOf(Character.toChars(codePoint))); |
| } else { |
| // argument of other class is not acceptable. |
| throw new IllegalFormatConversionException(formatToken |
| .getConversionType(), arg.getClass()); |
| } |
| } |
| return padding(result, startIndex); |
| } |
| |
| /* |
| * Transforms percent to a formatted string. Only '-' is legal flag. |
| * Precision is illegal. |
| */ |
| private String transformFromPercent() { |
| StringBuilder result = new StringBuilder("%"); //$NON-NLS-1$ |
| |
| int startIndex = 0; |
| int flags = formatToken.getFlags(); |
| |
| if (formatToken.isFlagSet(FormatToken.FLAG_MINUS) |
| && !formatToken.isWidthSet()) { |
| throw new MissingFormatWidthException("-" //$NON-NLS-1$ |
| + formatToken.getConversionType()); |
| } |
| |
| if (FormatToken.FLAGS_UNSET != flags |
| && FormatToken.FLAG_MINUS != flags) { |
| throw new FormatFlagsConversionMismatchException(formatToken |
| .getStrFlags(), formatToken.getConversionType()); |
| } |
| if (formatToken.isPrecisionSet()) { |
| throw new IllegalFormatPrecisionException(formatToken |
| .getPrecision()); |
| } |
| return padding(result, startIndex); |
| } |
| |
| /* |
| * Transforms line separator to a formatted string. Any flag, the width |
| * or the precision is illegal. |
| */ |
| private String transformFromLineSeparator() { |
| if (formatToken.isPrecisionSet()) { |
| throw new IllegalFormatPrecisionException(formatToken |
| .getPrecision()); |
| } |
| |
| if (formatToken.isWidthSet()) { |
| throw new IllegalFormatWidthException(formatToken.getWidth()); |
| } |
| |
| int flags = formatToken.getFlags(); |
| if (FormatToken.FLAGS_UNSET != flags) { |
| throw new IllegalFormatFlagsException(formatToken.getStrFlags()); |
| } |
| |
| if (null == lineSeparator) { |
| lineSeparator = AccessController |
| .doPrivileged(new PrivilegedAction<String>() { |
| |
| public String run() { |
| return System.getProperty("line.separator"); //$NON-NLS-1$ |
| } |
| }); |
| } |
| return lineSeparator; |
| } |
| |
| /* |
| * Pads characters to the formatted string. |
| */ |
| private String padding(StringBuilder source, int startIndex) { |
| int start = startIndex; |
| boolean paddingRight = formatToken |
| .isFlagSet(FormatToken.FLAG_MINUS); |
| char paddingChar = '\u0020';// space as padding char. |
| if (formatToken.isFlagSet(FormatToken.FLAG_ZERO)) { |
| if ('d' == formatToken.getConversionType()) { |
| paddingChar = getDecimalFormatSymbols().getZeroDigit(); |
| } else { |
| paddingChar = '0'; |
| } |
| } else { |
| // if padding char is space, always padding from the head |
| // location. |
| start = 0; |
| } |
| int width = formatToken.getWidth(); |
| int precision = formatToken.getPrecision(); |
| |
| int length = source.length(); |
| if (precision >= 0) { |
| length = Math.min(length, precision); |
| source.delete(length, source.length()); |
| } |
| if (width > 0) { |
| width = Math.max(source.length(), width); |
| } |
| if (length >= width) { |
| return source.toString(); |
| } |
| |
| char[] paddings = new char[width - length]; |
| Arrays.fill(paddings, paddingChar); |
| String insertString = new String(paddings); |
| |
| if (paddingRight) { |
| source.append(insertString); |
| } else { |
| source.insert(start, insertString); |
| } |
| return source.toString(); |
| } |
| |
| /* |
| * Transforms the Integer to a formatted string. |
| */ |
| private String transformFromInteger() { |
| int startIndex = 0; |
| boolean isNegative = false; |
| StringBuilder result = new StringBuilder(); |
| char currentConversionType = formatToken.getConversionType(); |
| long value; |
| |
| if (formatToken.isFlagSet(FormatToken.FLAG_MINUS) |
| || formatToken.isFlagSet(FormatToken.FLAG_ZERO)) { |
| if (!formatToken.isWidthSet()) { |
| throw new MissingFormatWidthException(formatToken |
| .getStrFlags()); |
| } |
| } |
| // Combination of '+' & ' ' is illegal. |
| if (formatToken.isFlagSet(FormatToken.FLAG_ADD) |
| && formatToken.isFlagSet(FormatToken.FLAG_SPACE)) { |
| throw new IllegalFormatFlagsException(formatToken.getStrFlags()); |
| } |
| if (formatToken.isPrecisionSet()) { |
| throw new IllegalFormatPrecisionException(formatToken |
| .getPrecision()); |
| } |
| 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 new IllegalFormatConversionException(formatToken |
| .getConversionType(), arg.getClass()); |
| } |
| if ('d' != currentConversionType) { |
| if (formatToken.isFlagSet(FormatToken.FLAG_ADD) |
| || formatToken.isFlagSet(FormatToken.FLAG_SPACE) |
| || formatToken.isFlagSet(FormatToken.FLAG_COMMA) |
| || formatToken.isFlagSet(FormatToken.FLAG_PARENTHESIS)) { |
| throw new FormatFlagsConversionMismatchException( |
| formatToken.getStrFlags(), formatToken |
| .getConversionType()); |
| } |
| } |
| |
| if (formatToken.isFlagSet(FormatToken.FLAG_SHARP)) { |
| if ('d' == currentConversionType) { |
| throw new FormatFlagsConversionMismatchException( |
| formatToken.getStrFlags(), formatToken |
| .getConversionType()); |
| } else if ('o' == currentConversionType) { |
| result.append("0"); //$NON-NLS-1$ |
| startIndex += 1; |
| } else { |
| result.append("0x"); //$NON-NLS-1$ |
| startIndex += 2; |
| } |
| } |
| |
| if (formatToken.isFlagSet(FormatToken.FLAG_MINUS) |
| && formatToken.isFlagSet(FormatToken.FLAG_ZERO)) { |
| throw new IllegalFormatFlagsException(formatToken.getStrFlags()); |
| } |
| |
| if (value < 0) { |
| isNegative = true; |
| } |
| |
| if ('d' == currentConversionType) { |
| NumberFormat numberFormat = getNumberFormat(); |
| if (formatToken.isFlagSet(FormatToken.FLAG_COMMA)) { |
| numberFormat.setGroupingUsed(true); |
| } else { |
| numberFormat.setGroupingUsed(false); |
| } |
| result.append(numberFormat.format(arg)); |
| } else { |
| long BYTE_MASK = 0x00000000000000FFL; |
| long SHORT_MASK = 0x000000000000FFFFL; |
| long INT_MASK = 0x00000000FFFFFFFFL; |
| if (isNegative) { |
| if (arg instanceof Byte) { |
| value &= BYTE_MASK; |
| } else if (arg instanceof Short) { |
| value &= SHORT_MASK; |
| } else if (arg instanceof Integer) { |
| value &= INT_MASK; |
| } |
| } |
| if ('o' == currentConversionType) { |
| result.append(Long.toOctalString(value)); |
| } else { |
| result.append(Long.toHexString(value)); |
| } |
| isNegative = false; |
| } |
| |
| if (!isNegative) { |
| if (formatToken.isFlagSet(FormatToken.FLAG_ADD)) { |
| result.insert(0, '+'); |
| startIndex += 1; |
| } |
| if (formatToken.isFlagSet(FormatToken.FLAG_SPACE)) { |
| result.insert(0, ' '); |
| startIndex += 1; |
| } |
| } |
| |
| /* pad paddingChar to the output */ |
| if (isNegative |
| && formatToken.isFlagSet(FormatToken.FLAG_PARENTHESIS)) { |
| result = wrapParentheses(result); |
| return result.toString(); |
| |
| } |
| if (isNegative && formatToken.isFlagSet(FormatToken.FLAG_ZERO)) { |
| startIndex++; |
| } |
| return padding(result, startIndex); |
| } |
| |
| /* |
| * add () to the output,if the value is negative and |
| * formatToken.FLAG_PARENTHESIS is set. 'result' is used as an in-out |
| * parameter. |
| */ |
| private StringBuilder wrapParentheses(StringBuilder result) { |
| // delete the '-' |
| result.deleteCharAt(0); |
| result.insert(0, '('); |
| if (formatToken.isFlagSet(FormatToken.FLAG_ZERO)) { |
| formatToken.setWidth(formatToken.getWidth() - 1); |
| padding(result, 1); |
| result.append(')'); |
| } else { |
| result.append(')'); |
| padding(result, 0); |
| } |
| return result; |
| } |
| |
| private String transformFromSpecialNumber() { |
| String source = null; |
| |
| if (!(arg instanceof Number) || arg instanceof BigDecimal) { |
| return null; |
| } |
| |
| Number number = (Number) arg; |
| double d = number.doubleValue(); |
| if (Double.isNaN(d)) { |
| source = "NaN"; //$NON-NLS-1$ |
| } else if (Double.isInfinite(d)) { |
| if (d >= 0) { |
| if (formatToken.isFlagSet(FormatToken.FLAG_ADD)) { |
| source = "+Infinity"; //$NON-NLS-1$ |
| } else if (formatToken.isFlagSet(FormatToken.FLAG_SPACE)) { |
| source = " Infinity"; //$NON-NLS-1$ |
| } else { |
| source = "Infinity"; //$NON-NLS-1$ |
| } |
| } else { |
| if (formatToken.isFlagSet(FormatToken.FLAG_PARENTHESIS)) { |
| source = "(Infinity)"; //$NON-NLS-1$ |
| } else { |
| source = "-Infinity"; //$NON-NLS-1$ |
| } |
| } |
| } |
| |
| if (null != source) { |
| formatToken.setPrecision(FormatToken.UNSET); |
| formatToken.setFlags(formatToken.getFlags() |
| & (~FormatToken.FLAG_ZERO)); |
| source = padding(new StringBuilder(source), 0); |
| } |
| return source; |
| } |
| |
| private String transformFromNull() { |
| formatToken.setFlags(formatToken.getFlags() |
| & (~FormatToken.FLAG_ZERO)); |
| return padding(new StringBuilder("null"), 0); //$NON-NLS-1$ |
| } |
| |
| /* |
| * Transforms a BigInteger to a formatted string. |
| */ |
| private String transformFromBigInteger() { |
| int startIndex = 0; |
| boolean isNegative = false; |
| StringBuilder result = new StringBuilder(); |
| BigInteger bigInt = (BigInteger) arg; |
| char currentConversionType = formatToken.getConversionType(); |
| |
| if (formatToken.isFlagSet(FormatToken.FLAG_MINUS) |
| || formatToken.isFlagSet(FormatToken.FLAG_ZERO)) { |
| if (!formatToken.isWidthSet()) { |
| throw new MissingFormatWidthException(formatToken |
| .getStrFlags()); |
| } |
| } |
| |
| // Combination of '+' & ' ' is illegal. |
| if (formatToken.isFlagSet(FormatToken.FLAG_ADD) |
| && formatToken.isFlagSet(FormatToken.FLAG_SPACE)) { |
| throw new IllegalFormatFlagsException(formatToken.getStrFlags()); |
| } |
| |
| // Combination of '-' & '0' is illegal. |
| if (formatToken.isFlagSet(FormatToken.FLAG_ZERO) |
| && formatToken.isFlagSet(FormatToken.FLAG_MINUS)) { |
| throw new IllegalFormatFlagsException(formatToken.getStrFlags()); |
| } |
| |
| if (formatToken.isPrecisionSet()) { |
| throw new IllegalFormatPrecisionException(formatToken |
| .getPrecision()); |
| } |
| |
| if ('d' != currentConversionType |
| && formatToken.isFlagSet(FormatToken.FLAG_COMMA)) { |
| throw new FormatFlagsConversionMismatchException(formatToken |
| .getStrFlags(), currentConversionType); |
| } |
| |
| if (formatToken.isFlagSet(FormatToken.FLAG_SHARP) |
| && 'd' == currentConversionType) { |
| throw new FormatFlagsConversionMismatchException(formatToken |
| .getStrFlags(), currentConversionType); |
| } |
| |
| if (null == bigInt) { |
| return transformFromNull(); |
| } |
| |
| isNegative = (bigInt.compareTo(BigInteger.ZERO) < 0); |
| |
| if ('d' == currentConversionType) { |
| NumberFormat numberFormat = getNumberFormat(); |
| boolean readableName = formatToken |
| .isFlagSet(FormatToken.FLAG_COMMA); |
| numberFormat.setGroupingUsed(readableName); |
| result.append(numberFormat.format(bigInt)); |
| } else if ('o' == currentConversionType) { |
| // 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.isFlagSet(FormatToken.FLAG_SHARP)) { |
| startIndex = isNegative ? 1 : 0; |
| if ('o' == currentConversionType) { |
| result.insert(startIndex, "0"); //$NON-NLS-1$ |
| startIndex += 1; |
| } else if ('x' == currentConversionType |
| || 'X' == currentConversionType) { |
| result.insert(startIndex, "0x"); //$NON-NLS-1$ |
| startIndex += 2; |
| } |
| } |
| |
| if (!isNegative) { |
| if (formatToken.isFlagSet(FormatToken.FLAG_ADD)) { |
| result.insert(0, '+'); |
| startIndex += 1; |
| } |
| if (formatToken.isFlagSet(FormatToken.FLAG_SPACE)) { |
| result.insert(0, ' '); |
| startIndex += 1; |
| } |
| } |
| |
| /* pad paddingChar to the output */ |
| if (isNegative |
| && formatToken.isFlagSet(FormatToken.FLAG_PARENTHESIS)) { |
| result = wrapParentheses(result); |
| return result.toString(); |
| |
| } |
| if (isNegative && formatToken.isFlagSet(FormatToken.FLAG_ZERO)) { |
| startIndex++; |
| } |
| return padding(result, startIndex); |
| } |
| |
| /* |
| * Transforms a Float,Double or BigDecimal to a formatted string. |
| */ |
| private String transformFromFloat() { |
| StringBuilder result = new StringBuilder(); |
| int startIndex = 0; |
| char currentConversionType = formatToken.getConversionType(); |
| |
| if (formatToken.isFlagSet(FormatToken.FLAG_MINUS |
| | FormatToken.FLAG_ZERO)) { |
| if (!formatToken.isWidthSet()) { |
| throw new MissingFormatWidthException(formatToken |
| .getStrFlags()); |
| } |
| } |
| |
| if (formatToken.isFlagSet(FormatToken.FLAG_ADD) |
| && formatToken.isFlagSet(FormatToken.FLAG_SPACE)) { |
| throw new IllegalFormatFlagsException(formatToken.getStrFlags()); |
| } |
| |
| if (formatToken.isFlagSet(FormatToken.FLAG_MINUS) |
| && formatToken.isFlagSet(FormatToken.FLAG_ZERO)) { |
| throw new IllegalFormatFlagsException(formatToken.getStrFlags()); |
| } |
| |
| if ('e' == Character.toLowerCase(currentConversionType)) { |
| if (formatToken.isFlagSet(FormatToken.FLAG_COMMA)) { |
| throw new FormatFlagsConversionMismatchException( |
| formatToken.getStrFlags(), currentConversionType); |
| } |
| } |
| |
| if ('g' == Character.toLowerCase(currentConversionType)) { |
| if (formatToken.isFlagSet(FormatToken.FLAG_SHARP)) { |
| throw new FormatFlagsConversionMismatchException( |
| formatToken.getStrFlags(), currentConversionType); |
| } |
| } |
| |
| if ('a' == Character.toLowerCase(currentConversionType)) { |
| if (formatToken.isFlagSet(FormatToken.FLAG_COMMA) |
| || formatToken.isFlagSet(FormatToken.FLAG_PARENTHESIS)) { |
| throw new FormatFlagsConversionMismatchException( |
| formatToken.getStrFlags(), currentConversionType); |
| } |
| } |
| |
| if (null == arg) { |
| return transformFromNull(); |
| } |
| |
| if (!(arg instanceof Float || arg instanceof Double || arg instanceof BigDecimal)) { |
| throw new IllegalFormatConversionException( |
| currentConversionType, arg.getClass()); |
| } |
| |
| String specialNumberResult = transformFromSpecialNumber(); |
| if (null != specialNumberResult) { |
| return specialNumberResult; |
| } |
| |
| if ('a' != Character.toLowerCase(currentConversionType)) { |
| formatToken |
| .setPrecision(formatToken.isPrecisionSet() ? formatToken |
| .getPrecision() |
| : FormatToken.DEFAULT_PRECISION); |
| } |
| // output result |
| FloatUtil floatUtil = new FloatUtil(result, formatToken, |
| (DecimalFormat) getNumberFormat(), arg); |
| floatUtil.transform(formatToken, result); |
| |
| formatToken.setPrecision(FormatToken.UNSET); |
| |
| if (getDecimalFormatSymbols().getMinusSign() == result.charAt(0)) { |
| if (formatToken.isFlagSet(FormatToken.FLAG_PARENTHESIS)) { |
| result = wrapParentheses(result); |
| return result.toString(); |
| } |
| } else { |
| if (formatToken.isFlagSet(FormatToken.FLAG_SPACE)) { |
| result.insert(0, ' '); |
| startIndex++; |
| } |
| if (formatToken.isFlagSet(FormatToken.FLAG_ADD)) { |
| result.insert(0, floatUtil.getAddSign()); |
| startIndex++; |
| } |
| } |
| |
| char firstChar = result.charAt(0); |
| if (formatToken.isFlagSet(FormatToken.FLAG_ZERO) |
| && (firstChar == floatUtil.getAddSign() || firstChar == floatUtil |
| .getMinusSign())) { |
| startIndex = 1; |
| } |
| |
| if ('a' == Character.toLowerCase(currentConversionType)) { |
| startIndex += 2; |
| } |
| return padding(result, startIndex); |
| } |
| |
| /* |
| * Transforms a Date to a formatted string. |
| */ |
| private String transformFromDateTime() { |
| int startIndex = 0; |
| char currentConversionType = formatToken.getConversionType(); |
| |
| if (formatToken.isPrecisionSet()) { |
| throw new IllegalFormatPrecisionException(formatToken |
| .getPrecision()); |
| } |
| |
| if (formatToken.isFlagSet(FormatToken.FLAG_SHARP)) { |
| throw new FormatFlagsConversionMismatchException(formatToken |
| .getStrFlags(), currentConversionType); |
| } |
| |
| if (formatToken.isFlagSet(FormatToken.FLAG_MINUS) |
| && FormatToken.UNSET == formatToken.getWidth()) { |
| throw new MissingFormatWidthException("-" //$NON-NLS-1$ |
| + currentConversionType); |
| } |
| |
| if (null == arg) { |
| 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 new IllegalFormatConversionException( |
| currentConversionType, arg.getClass()); |
| } |
| calendar = Calendar.getInstance(locale); |
| calendar.setTime(date); |
| } |
| |
| if (null == dateTimeUtil) { |
| dateTimeUtil = new DateTimeUtil(locale); |
| } |
| StringBuilder result = new StringBuilder(); |
| // output result |
| dateTimeUtil.transform(formatToken, calendar, result); |
| return padding(result, startIndex); |
| } |
| } |
| |
| private static class FloatUtil { |
| private StringBuilder result; |
| |
| private DecimalFormat decimalFormat; |
| |
| private FormatToken formatToken; |
| |
| private Object argument; |
| |
| private char minusSign; |
| |
| FloatUtil(StringBuilder result, FormatToken formatToken, |
| DecimalFormat decimalFormat, Object argument) { |
| this.result = result; |
| this.formatToken = formatToken; |
| this.decimalFormat = decimalFormat; |
| this.argument = argument; |
| this.minusSign = decimalFormat.getDecimalFormatSymbols() |
| .getMinusSign(); |
| } |
| |
| void transform(FormatToken aFormatToken, StringBuilder aResult) { |
| this.result = aResult; |
| this.formatToken = aFormatToken; |
| switch (formatToken.getConversionType()) { |
| case 'e': |
| case 'E': { |
| transform_e(); |
| break; |
| } |
| case 'f': { |
| transform_f(); |
| break; |
| } |
| case 'g': |
| case 'G': { |
| transform_g(); |
| break; |
| } |
| case 'a': |
| case 'A': { |
| transform_a(); |
| break; |
| } |
| default: { |
| throw new UnknownFormatConversionException(String |
| .valueOf(formatToken.getConversionType())); |
| } |
| } |
| } |
| |
| char getMinusSign() { |
| return minusSign; |
| } |
| |
| char getAddSign() { |
| return '+'; |
| } |
| |
| void transform_e() { |
| StringBuilder pattern = new StringBuilder(); |
| pattern.append('0'); |
| if (formatToken.getPrecision() > 0) { |
| pattern.append('.'); |
| char[] zeros = new char[formatToken.getPrecision()]; |
| Arrays.fill(zeros, '0'); |
| pattern.append(zeros); |
| } |
| pattern.append('E'); |
| pattern.append("+00"); //$NON-NLS-1$ |
| decimalFormat.applyPattern(pattern.toString()); |
| String formattedString = decimalFormat.format(argument); |
| result.append(formattedString.replace('E', 'e')); |
| |
| // if the flag is sharp and decimal seperator is always given |
| // out. |
| if (formatToken.isFlagSet(FormatToken.FLAG_SHARP) |
| && 0 == formatToken.getPrecision()) { |
| int indexOfE = result.indexOf("e"); //$NON-NLS-1$ |
| char dot = decimalFormat.getDecimalFormatSymbols() |
| .getDecimalSeparator(); |
| result.insert(indexOfE, dot); |
| } |
| } |
| |
| void transform_g() { |
| int precision = formatToken.getPrecision(); |
| precision = (0 == precision ? 1 : precision); |
| formatToken.setPrecision(precision); |
| |
| if (0.0 == ((Number) argument).doubleValue()) { |
| precision--; |
| formatToken.setPrecision(precision); |
| transform_f(); |
| return; |
| } |
| |
| boolean requireScientificRepresentation = true; |
| double d = ((Number) argument).doubleValue(); |
| d = Math.abs(d); |
| if (Double.isInfinite(d)) { |
| precision = formatToken.getPrecision(); |
| precision--; |
| formatToken.setPrecision(precision); |
| transform_e(); |
| 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(); |
| b.movePointLeft(4); |
| if (d >= Math.pow(10, -4) && d < 1) { |
| requireScientificRepresentation = false; |
| precision += 4 - String.valueOf(l).length(); |
| l = b.movePointRight(precision + 1).longValue(); |
| b.movePointLeft(precision + 1); |
| if (String.valueOf(l).length() <= formatToken |
| .getPrecision()) { |
| precision++; |
| } |
| l = b.movePointRight(precision).longValue(); |
| b.movePointLeft(precision); |
| if (l >= Math.pow(10, precision - 4)) { |
| formatToken.setPrecision(precision); |
| } |
| } |
| } |
| if (requireScientificRepresentation) { |
| precision = formatToken.getPrecision(); |
| precision--; |
| formatToken.setPrecision(precision); |
| transform_e(); |
| } else { |
| transform_f(); |
| } |
| |
| } |
| |
| void transform_f() { |
| StringBuilder pattern = new StringBuilder(); |
| if (formatToken.isFlagSet(FormatToken.FLAG_COMMA)) { |
| pattern.append(','); |
| int groupingSize = decimalFormat.getGroupingSize(); |
| if (groupingSize > 1) { |
| char[] sharps = new char[groupingSize - 1]; |
| Arrays.fill(sharps, '#'); |
| pattern.append(sharps); |
| } |
| } |
| |
| pattern.append(0); |
| |
| if (formatToken.getPrecision() > 0) { |
| pattern.append('.'); |
| char[] zeros = new char[formatToken.getPrecision()]; |
| Arrays.fill(zeros, '0'); |
| pattern.append(zeros); |
| } |
| decimalFormat.applyPattern(pattern.toString()); |
| result.append(decimalFormat.format(argument)); |
| // if the flag is sharp and decimal seperator is always given |
| // out. |
| if (formatToken.isFlagSet(FormatToken.FLAG_SHARP) |
| && 0 == formatToken.getPrecision()) { |
| char dot = decimalFormat.getDecimalFormatSymbols() |
| .getDecimalSeparator(); |
| result.append(dot); |
| } |
| |
| } |
| |
| void transform_a() { |
| char currentConversionType = formatToken.getConversionType(); |
| |
| if (argument instanceof Float) { |
| Float F = (Float) argument; |
| result.append(Float.toHexString(F.floatValue())); |
| |
| } else if (argument instanceof Double) { |
| Double D = (Double) argument; |
| result.append(Double.toHexString(D.doubleValue())); |
| } else { |
| // BigInteger is not supported. |
| throw new IllegalFormatConversionException( |
| currentConversionType, argument.getClass()); |
| } |
| |
| if (!formatToken.isPrecisionSet()) { |
| return; |
| } |
| |
| int precision = formatToken.getPrecision(); |
| precision = (0 == precision ? 1 : precision); |
| int indexOfFirstFracitoanlDigit = result.indexOf(".") + 1; //$NON-NLS-1$ |
| int indexOfP = result.indexOf("p"); //$NON-NLS-1$ |
| int fractionalLength = indexOfP - indexOfFirstFracitoanlDigit; |
| |
| if (fractionalLength == precision) { |
| return; |
| } |
| |
| if (fractionalLength < precision) { |
| char zeros[] = new char[precision - fractionalLength]; |
| Arrays.fill(zeros, '0'); |
| result.insert(indexOfP, zeros); |
| return; |
| } |
| result.delete(indexOfFirstFracitoanlDigit + precision, indexOfP); |
| } |
| } |
| |
| private static class DateTimeUtil { |
| private Calendar calendar; |
| |
| private Locale locale; |
| |
| private StringBuilder result; |
| |
| private DateFormatSymbols dateFormatSymbols; |
| |
| DateTimeUtil(Locale locale) { |
| this.locale = locale; |
| } |
| |
| void transform(FormatToken formatToken, Calendar aCalendar, |
| StringBuilder aResult) { |
| this.result = aResult; |
| this.calendar = aCalendar; |
| char suffix = formatToken.getDateSuffix(); |
| |
| switch (suffix) { |
| case 'H': { |
| transform_H(); |
| break; |
| } |
| case 'I': { |
| transform_I(); |
| break; |
| } |
| case 'M': { |
| transform_M(); |
| break; |
| } |
| case 'S': { |
| transform_S(); |
| break; |
| } |
| case 'L': { |
| transform_L(); |
| break; |
| } |
| case 'N': { |
| transform_N(); |
| break; |
| } |
| case 'k': { |
| transform_k(); |
| break; |
| } |
| case 'l': { |
| transform_l(); |
| break; |
| } |
| case 'p': { |
| transform_p(true); |
| break; |
| } |
| case 's': { |
| transform_s(); |
| break; |
| } |
| case 'z': { |
| transform_z(); |
| break; |
| } |
| case 'Z': { |
| transform_Z(); |
| break; |
| } |
| case 'Q': { |
| transform_Q(); |
| break; |
| } |
| case 'B': { |
| transform_B(); |
| break; |
| } |
| case 'b': |
| case 'h': { |
| transform_b(); |
| break; |
| } |
| case 'A': { |
| transform_A(); |
| break; |
| } |
| case 'a': { |
| transform_a(); |
| break; |
| } |
| case 'C': { |
| transform_C(); |
| break; |
| } |
| case 'Y': { |
| transform_Y(); |
| break; |
| } |
| case 'y': { |
| transform_y(); |
| break; |
| } |
| case 'j': { |
| transform_j(); |
| break; |
| } |
| case 'm': { |
| transform_m(); |
| break; |
| } |
| case 'd': { |
| transform_d(); |
| break; |
| } |
| case 'e': { |
| transform_e(); |
| break; |
| } |
| case 'R': { |
| transform_R(); |
| break; |
| } |
| |
| case 'T': { |
| transform_T(); |
| break; |
| } |
| case 'r': { |
| transform_r(); |
| break; |
| } |
| case 'D': { |
| transform_D(); |
| break; |
| } |
| case 'F': { |
| transform_F(); |
| break; |
| } |
| case 'c': { |
| transform_c(); |
| break; |
| } |
| default: { |
| throw new UnknownFormatConversionException(String |
| .valueOf(formatToken.getConversionType()) |
| + formatToken.getDateSuffix()); |
| } |
| } |
| } |
| |
| private void transform_e() { |
| int day = calendar.get(Calendar.DAY_OF_MONTH); |
| result.append(day); |
| } |
| |
| private void transform_d() { |
| int day = calendar.get(Calendar.DAY_OF_MONTH); |
| result.append(paddingZeros(day, 2)); |
| } |
| |
| private void transform_m() { |
| int month = calendar.get(Calendar.MONTH); |
| // The returned month starts from zero, which needs to be |
| // incremented by 1. |
| month++; |
| result.append(paddingZeros(month, 2)); |
| } |
| |
| private void transform_j() { |
| int day = calendar.get(Calendar.DAY_OF_YEAR); |
| result.append(paddingZeros(day, 3)); |
| } |
| |
| private void transform_y() { |
| int year = calendar.get(Calendar.YEAR); |
| year %= 100; |
| result.append(paddingZeros(year, 2)); |
| } |
| |
| private void transform_Y() { |
| int year = calendar.get(Calendar.YEAR); |
| result.append(paddingZeros(year, 4)); |
| } |
| |
| private void transform_C() { |
| int year = calendar.get(Calendar.YEAR); |
| year /= 100; |
| result.append(paddingZeros(year, 2)); |
| } |
| |
| private void transform_a() { |
| int day = calendar.get(Calendar.DAY_OF_WEEK); |
| result.append(getDateFormatSymbols().getShortWeekdays()[day]); |
| } |
| |
| private void transform_A() { |
| int day = calendar.get(Calendar.DAY_OF_WEEK); |
| result.append(getDateFormatSymbols().getWeekdays()[day]); |
| } |
| |
| private void transform_b() { |
| int month = calendar.get(Calendar.MONTH); |
| result.append(getDateFormatSymbols().getShortMonths()[month]); |
| } |
| |
| private void transform_B() { |
| int month = calendar.get(Calendar.MONTH); |
| result.append(getDateFormatSymbols().getMonths()[month]); |
| } |
| |
| private void transform_Q() { |
| long milliSeconds = calendar.getTimeInMillis(); |
| result.append(milliSeconds); |
| } |
| |
| private void transform_s() { |
| long milliSeconds = calendar.getTimeInMillis(); |
| milliSeconds /= 1000; |
| result.append(milliSeconds); |
| } |
| |
| private void transform_Z() { |
| TimeZone timeZone = calendar.getTimeZone(); |
| result.append(timeZone |
| .getDisplayName( |
| timeZone.inDaylightTime(calendar.getTime()), |
| TimeZone.SHORT, locale)); |
| } |
| |
| private void transform_z() { |
| int zoneOffset = calendar.get(Calendar.ZONE_OFFSET); |
| zoneOffset /= 3600000; |
| zoneOffset *= 100; |
| if (zoneOffset >= 0) { |
| result.append('+'); |
| } |
| result.append(paddingZeros(zoneOffset, 4)); |
| } |
| |
| private void transform_p(boolean isLowerCase) { |
| int i = calendar.get(Calendar.AM_PM); |
| String s = getDateFormatSymbols().getAmPmStrings()[i]; |
| if (isLowerCase) { |
| s = s.toLowerCase(locale); |
| } |
| result.append(s); |
| } |
| |
| private void transform_N() { |
| // TODO System.nanoTime(); |
| long nanosecond = calendar.get(Calendar.MILLISECOND) * 1000000L; |
| result.append(paddingZeros(nanosecond, 9)); |
| } |
| |
| private void transform_L() { |
| int millisecond = calendar.get(Calendar.MILLISECOND); |
| result.append(paddingZeros(millisecond, 3)); |
| } |
| |
| private void transform_S() { |
| int second = calendar.get(Calendar.SECOND); |
| result.append(paddingZeros(second, 2)); |
| } |
| |
| private void transform_M() { |
| int minute = calendar.get(Calendar.MINUTE); |
| result.append(paddingZeros(minute, 2)); |
| } |
| |
| private void transform_l() { |
| int hour = calendar.get(Calendar.HOUR); |
| if (0 == hour) { |
| hour = 12; |
| } |
| result.append(hour); |
| } |
| |
| private void transform_k() { |
| int hour = calendar.get(Calendar.HOUR_OF_DAY); |
| result.append(hour); |
| } |
| |
| private void transform_I() { |
| int hour = calendar.get(Calendar.HOUR); |
| if (0 == hour) { |
| hour = 12; |
| } |
| result.append(paddingZeros(hour, 2)); |
| } |
| |
| private void transform_H() { |
| int hour = calendar.get(Calendar.HOUR_OF_DAY); |
| result.append(paddingZeros(hour, 2)); |
| } |
| |
| private void transform_R() { |
| transform_H(); |
| result.append(':'); |
| transform_M(); |
| } |
| |
| private void transform_T() { |
| transform_H(); |
| result.append(':'); |
| transform_M(); |
| result.append(':'); |
| transform_S(); |
| } |
| |
| private void transform_r() { |
| transform_I(); |
| result.append(':'); |
| transform_M(); |
| result.append(':'); |
| transform_S(); |
| result.append(' '); |
| transform_p(false); |
| } |
| |
| private void transform_D() { |
| transform_m(); |
| result.append('/'); |
| transform_d(); |
| result.append('/'); |
| transform_y(); |
| } |
| |
| private void transform_F() { |
| transform_Y(); |
| result.append('-'); |
| transform_m(); |
| result.append('-'); |
| transform_d(); |
| } |
| |
| private void transform_c() { |
| transform_a(); |
| result.append(' '); |
| transform_b(); |
| result.append(' '); |
| transform_d(); |
| result.append(' '); |
| transform_T(); |
| result.append(' '); |
| transform_Z(); |
| result.append(' '); |
| transform_Y(); |
| } |
| |
| private static String paddingZeros(long number, int length) { |
| int len = length; |
| StringBuilder result = new StringBuilder(); |
| result.append(number); |
| int startIndex = 0; |
| if (number < 0) { |
| len++; |
| startIndex = 1; |
| } |
| len -= result.length(); |
| if (len > 0) { |
| char[] zeros = new char[len]; |
| Arrays.fill(zeros, '0'); |
| result.insert(startIndex, zeros); |
| } |
| return result.toString(); |
| } |
| |
| private DateFormatSymbols getDateFormatSymbols() { |
| if (null == dateFormatSymbols) { |
| dateFormatSymbols = new DateFormatSymbols(locale); |
| } |
| return dateFormatSymbols; |
| } |
| } |
| |
| private static class ParserStateMachine { |
| |
| private static final char EOS = (char) -1; |
| |
| private static final int EXIT_STATE = 0; |
| |
| private static final int ENTRY_STATE = 1; |
| |
| private static final int START_CONVERSION_STATE = 2; |
| |
| private static final int FLAGS_STATE = 3; |
| |
| private static final int WIDTH_STATE = 4; |
| |
| private static final int PRECISION_STATE = 5; |
| |
| private static final int CONVERSION_TYPE_STATE = 6; |
| |
| private static final int SUFFIX_STATE = 7; |
| |
| private FormatToken token; |
| |
| private int state = ENTRY_STATE; |
| |
| private char currentChar = 0; |
| |
| private CharBuffer format = null; |
| |
| ParserStateMachine(CharBuffer format) { |
| this.format = format; |
| } |
| |
| void reset() { |
| this.currentChar = (char) FormatToken.UNSET; |
| this.state = ENTRY_STATE; |
| this.token = null; |
| } |
| |
| /* |
| * Gets the information about the current format token. Information is |
| * recorded in the FormatToken returned and the position of the stream |
| * for the format string will be advanced till the next format token. |
| */ |
| FormatToken getNextFormatToken() { |
| token = new FormatToken(); |
| token.setFormatStringStartIndex(format.position()); |
| |
| // FINITE AUTOMATIC MACHINE |
| while (true) { |
| |
| if (ParserStateMachine.EXIT_STATE != state) { |
| // exit state does not need to get next char |
| currentChar = getNextFormatChar(); |
| if (EOS == currentChar |
| && ParserStateMachine.ENTRY_STATE != state) { |
| throw new UnknownFormatConversionException( |
| getFormatString()); |
| } |
| } |
| |
| switch (state) { |
| // exit state |
| case ParserStateMachine.EXIT_STATE: { |
| process_EXIT_STATE(); |
| return token; |
| } |
| // plain text state, not yet applied converter |
| case ParserStateMachine.ENTRY_STATE: { |
| process_ENTRY_STATE(); |
| break; |
| } |
| // begins converted string |
| case ParserStateMachine.START_CONVERSION_STATE: { |
| process_START_CONVERSION_STATE(); |
| break; |
| } |
| case ParserStateMachine.FLAGS_STATE: { |
| process_FlAGS_STATE(); |
| break; |
| } |
| case ParserStateMachine.WIDTH_STATE: { |
| process_WIDTH_STATE(); |
| break; |
| } |
| case ParserStateMachine.PRECISION_STATE: { |
| process_PRECISION_STATE(); |
| break; |
| } |
| case ParserStateMachine.CONVERSION_TYPE_STATE: { |
| process_CONVERSION_TYPE_STATE(); |
| break; |
| } |
| case ParserStateMachine.SUFFIX_STATE: { |
| process_SUFFIX_STATE(); |
| break; |
| } |
| } |
| } |
| } |
| |
| /* |
| * Gets next char from the format string. |
| */ |
| private char getNextFormatChar() { |
| if (format.hasRemaining()) { |
| return format.get(); |
| } |
| return EOS; |
| } |
| |
| private String getFormatString() { |
| int end = format.position(); |
| format.rewind(); |
| String formatString = format.subSequence( |
| token.getFormatStringStartIndex(), end).toString(); |
| format.position(end); |
| return formatString; |
| } |
| |
| private void process_ENTRY_STATE() { |
| if (EOS == currentChar) { |
| state = ParserStateMachine.EXIT_STATE; |
| } else if ('%' == currentChar) { |
| // change to conversion type state |
| state = START_CONVERSION_STATE; |
| } |
| // else remains in ENTRY_STATE |
| } |
| |
| private void process_START_CONVERSION_STATE() { |
| if (Character.isDigit(currentChar)) { |
| int position = format.position() - 1; |
| int number = parseInt(format); |
| char nextChar = 0; |
| if (format.hasRemaining()) { |
| nextChar = format.get(); |
| } |
| if ('$' == nextChar) { |
| // the digital sequence stands for the argument |
| // index. |
| int argIndex = number; |
| // k$ stands for the argument whose index is k-1 except that |
| // 0$ and 1$ both stands for the first element. |
| if (argIndex > 0) { |
| token.setArgIndex(argIndex - 1); |
| } else if (argIndex == FormatToken.UNSET) { |
| throw new MissingFormatArgumentException( |
| getFormatString()); |
| } |
| state = FLAGS_STATE; |
| } else { |
| // the digital zero stands for one format flag. |
| if ('0' == currentChar) { |
| state = FLAGS_STATE; |
| format.position(position); |
| } else { |
| // the digital sequence stands for the width. |
| state = WIDTH_STATE; |
| // do not get the next char. |
| format.position(format.position() - 1); |
| token.setWidth(number); |
| } |
| } |
| currentChar = nextChar; |
| } else if ('<' == currentChar) { |
| state = FLAGS_STATE; |
| token.setArgIndex(FormatToken.LAST_ARGUMENT_INDEX); |
| } else { |
| state = FLAGS_STATE; |
| // do not get the next char. |
| format.position(format.position() - 1); |
| } |
| |
| } |
| |
| private void process_FlAGS_STATE() { |
| if (token.setFlag(currentChar)) { |
| // remains in FLAGS_STATE |
| } else if (Character.isDigit(currentChar)) { |
| token.setWidth(parseInt(format)); |
| state = WIDTH_STATE; |
| } else if ('.' == currentChar) { |
| state = PRECISION_STATE; |
| } else { |
| state = CONVERSION_TYPE_STATE; |
| // do not get the next char. |
| format.position(format.position() - 1); |
| } |
| } |
| |
| private void process_WIDTH_STATE() { |
| if ('.' == currentChar) { |
| state = PRECISION_STATE; |
| } else { |
| state = CONVERSION_TYPE_STATE; |
| // do not get the next char. |
| format.position(format.position() - 1); |
| } |
| } |
| |
| private void process_PRECISION_STATE() { |
| if (Character.isDigit(currentChar)) { |
| token.setPrecision(parseInt(format)); |
| } else { |
| // the precision is required but not given by the |
| // format string. |
| throw new UnknownFormatConversionException(getFormatString()); |
| } |
| state = CONVERSION_TYPE_STATE; |
| } |
| |
| private void process_CONVERSION_TYPE_STATE() { |
| token.setConversionType(currentChar); |
| if ('t' == currentChar || 'T' == currentChar) { |
| state = SUFFIX_STATE; |
| } else { |
| state = EXIT_STATE; |
| } |
| |
| } |
| |
| private void process_SUFFIX_STATE() { |
| token.setDateSuffix(currentChar); |
| state = EXIT_STATE; |
| } |
| |
| private void process_EXIT_STATE() { |
| token.setPlainText(getFormatString()); |
| } |
| |
| /* |
| * Parses integer value from the given buffer |
| */ |
| private int parseInt(CharBuffer buffer) { |
| int start = buffer.position() - 1; |
| int end = buffer.limit(); |
| while (buffer.hasRemaining()) { |
| if (!Character.isDigit(buffer.get())) { |
| end = buffer.position() - 1; |
| break; |
| } |
| } |
| buffer.position(0); |
| String intStr = buffer.subSequence(start, end).toString(); |
| buffer.position(end); |
| try { |
| return Integer.parseInt(intStr); |
| } catch (NumberFormatException e) { |
| return FormatToken.UNSET; |
| } |
| } |
| } |
| } |