/* 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). *

* 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. *

* Format strings 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. *

* Format specifiers (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. *

* Not all combinations of argument index, flags, width, precision, and conversion type * are valid. *

* Argument index. 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"}. *

* Flags. The available flags are: *

* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
Flags
{@code ,}Use grouping separators for large numbers. (Decimal only.){@code format("%,d", 1024);}{@code 1,234}
{@code +}Always show sign. (Decimal only.){@code format("%+d, %+4d", 5, 5);}
+5,   +5
{@code }A space indicates that non-negative numbers * should have a leading space. (Decimal only.){@code format("x% d% 5d", 4, 4);}
x 4    4
{@code (}Put parentheses around negative numbers. (Decimal only.){@code format("%(d, %(d, %(6d", 12, -12, -12);}
12, (12),   (12)
{@code -}Left-justify. (Requires width.){@code format("%-6dx", 5);}
{@code format("%-3C, %3C", 'd', 0x65);}
5      x

D  ,   E
{@code 0}Pad the number with leading zeros. (Requires width.){@code format("%07d, %03d", 4, 5555);}{@code 0000004, 5555}
{@code #}Alternate form. (Octal and hex only.) {@code format("%o %#o", 010, 010);}
{@code format("%x %#x", 0x12, 0x12);}
{@code 10 010}
{@code 12 0x12}
*

* Width. 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. *

* Precision. 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}. *

* Conversion type. 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. *

* 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). *

* This table shows the available single-character (non-date/time) conversion types: * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* String conversions *
* All types are acceptable arguments. Values of type {@link Formattable} have their * {@code formatTo} method invoked; all other types use {@code toString}. *
{@code s}String.{@code format("%s %s", "hello", "Hello");}{@code hello Hello}
{@code S}Uppercase string.{@code format("%S %S", "hello", "Hello");}{@code HELLO HELLO}
* Character conversions *
* Byte, Character, Short, and Integer (and primitives that box to those types) are all acceptable * as character arguments. Any other type is an error. *
{@code c}Character.{@code format("%c %c", 'd', 'E');}{@code d E}
{@code C}Uppercase character.{@code format("%C %C", 'd', 'E');}{@code D E}
* Integer conversions *
* 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. *
{@code d}Decimal.{@code format("%d", 26);}{@code 26}
{@code o}Octal.{@code format("%o", 032);}{@code 32}
{@code x}, {@code X}Hexadecimal.{@code format("%x %X", 0x1a, 0x1a);}{@code 1a 1A}
Floating-point conversions *
* Float, Double, and BigDecimal (and primitives that box to those types) are all acceptable as * floating-point arguments. Any other type is an error. *
{@code f}Decimal floating point.
format("%f", 123.456f);
format("%.1f", 123.456f);
format("%1.5f", 123.456f);
format("%10f", 123.456f);
format("%6.0f", 123.456f);
123.456001
123.5
123.45600
123.456001
   123
{@code e}, {@code E}Engineering/exponential floating point.
format("%e", 123.456f);
format("%.1e", 123.456f);
format("%1.5E", 123.456f);
format("%10E", 123.456f);
format("%6.0E", 123.456f);
1.234560e+02
1.2e+02
1.23456E+02
1.234560E+02
 1E+02
{@code g}, {@code G}Decimal or engineering, depending on the magnitude of the value.{@code format("%g %g", 0.123, 0.0000123);}{@code 0.123000 1.23000e-05}
{@code a}, {@code A}Hexadecimal floating point.{@code format("%a", 123.456f);}{@code 0x1.edd2f2p6}
* Boolean conversion *
* Accepts Boolean values. {@code null} is considered false, and instances of all other * types are considered true. *
{@code b}, {@code B}Boolean.{@code format("%b %b", true, false);}
{@code format("%B %B", true, false);}
{@code format("%b", null);}
{@code format("%b", "hello");}
{@code true false}
{@code TRUE FALSE}
{@code false}
{@code true}
* Hash code conversion *
* Invokes {@code hashCode} on its argument, which may be of any type. *
{@code h}, {@code H}Hexadecimal hash code.{@code format("%h", this);}
{@code format("%H", this);}
{@code format("%h", null);}
{@code 190d11}
{@code 190D11}
{@code null}
* Zero-argument conversions
{@code %}A literal % character.{@code format("%d%%", 50);}{@code 50%}
{@code n}Newline. (The value of {@link System#lineSeparator}.){@code format("first%nsecond");}{@code first\nsecond}
*

* 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. *

* The best non-localized form is ISO 8601, * 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). *

* This table shows the date/time conversions, but you should use {@link java.text.SimpleDateFormat} * instead: * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
Date/time conversions *
* 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. * Use {@link java.text.SimpleDateFormat} instead. *
{@code ta}Localized weekday name (abbreviated).{@code format("%ta", cal, cal);}{@code Tue}
{@code tA}Localized weekday name (full).{@code format("%tA", cal, cal);}{@code Tuesday}
{@code tb}Localized month name (abbreviated).{@code format("%tb", cal);}{@code Apr}
{@code tB}Localized month name (full).{@code format("%tB", cal);}{@code April}
{@code tc}C library asctime(3)-like output. Do not use.{@code format("%tc", cal);}{@code Tue Apr 01 16:19:17 CEST 2008}
{@code tC}2-digit century.{@code format("%tC", cal);}{@code 20}
{@code td}2-digit day of month (01-31).{@code format("%td", cal);}{@code 01}
{@code tD}Ambiguous US date format (MM/DD/YY). Do not use.{@code format("%tD", cal);}{@code 04/01/08}
{@code te}Day of month (1-31).{@code format("%te", cal);}{@code 1}
{@code tF}Full date in ISO 8601 format (YYYY-MM-DD).{@code format("%tF", cal);}{@code 2008-04-01}
{@code th}Synonym for {@code %tb}.
{@code tH}2-digit 24-hour hour of day (00-23).{@code format("%tH", cal);}{@code 16}
{@code tI}2-digit 12-hour hour of day (01-12).{@code format("%tI", cal);}{@code 04}
{@code tj}3-digit day of year (001-366).{@code format("%tj", cal);}{@code 092}
{@code tk}24-hour hour of day (0-23).{@code format("%tk", cal);}{@code 16}
{@code tl}12-hour hour of day (1-12).{@code format("%tl", cal);}{@code 4}
{@code tL}Milliseconds.{@code format("%tL", cal);}{@code 359}
{@code tm}2-digit month of year (01-12).{@code format("%tm", cal);}{@code 04}
{@code tM}2-digit minute.{@code format("%tM", cal);}{@code 08}
{@code tN}Nanoseconds.{@code format("%tN", cal);}{@code 359000000}
{@code tp}a.m. or p.m.{@code format("%tp %Tp", cal, cal);}{@code pm PM}
{@code tQ}Milliseconds since the epoch.{@code format("%tQ", cal);}{@code 1207059412656}
{@code tr}Full 12-hour time ({@code %tI:%tM:%tS %Tp}).{@code format("%tr", cal);}{@code 04:15:32 PM}
{@code tR}Short 24-hour time ({@code %tH:%tM}).{@code format("%tR", cal);}{@code 16:15}
{@code ts}Seconds since the epoch.{@code format("%ts", cal);}{@code 1207059412}
{@code tS}2-digit seconds (00-60).{@code format("%tS", cal);}{@code 17}
{@code tT}Full 24-hour time ({@code %tH:%tM:%tS}).{@code format("%tT", cal);}{@code 16:15:32}
{@code ty}2-digit year (00-99).{@code format("%ty", cal);}{@code 08}
{@code tY}4-digit year.{@code format("%tY", cal);}{@code 2008}
{@code tz}Time zone GMT offset.{@code format("%tz", cal);}{@code +0100}
{@code tZ}Localized time zone abbreviation.{@code format("%tZ", cal);}{@code CEST}
*

* 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. *

Number localization. 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. *

Thread safety. 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 = new ThreadLocal() { @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}. * *

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} used is the user's default locale. * See "Be wary of the default locale". */ public Formatter() { this(new StringBuilder(), Locale.getDefault()); } /** * Constructs a {@code Formatter} whose output will be written to the * specified {@code Appendable}. * *

The {@code Locale} used is the user's default locale. * See "Be wary of the default 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 (a == null) { 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} used is the user's default locale. * See "Be wary of the default 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. */ 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} used is the user's default locale. * See "Be wary of the default 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 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. * *

The {@code Locale} used is the user's default locale. * See "Be wary of the default 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}. */ 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} used is the user's default locale. * See "Be wary of the default 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 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}. * *

The charset of the {@code Formatter} is the default charset. * *

The {@code Locale} used is the user's default locale. * See "Be wary of the default locale". * * @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}. * *

The {@code Locale} used is the user's default locale. * See "Be wary of the default 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 { out = new BufferedWriter(new OutputStreamWriter(os, csn)); 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} used is the user's default locale. * See "Be wary of the default 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 (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 (startsWithMinusSign(result, 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 == '+' || startsWithMinusSign(result, localeData.minusSign))) { startIndex = localeData.minusSign.length(); } if (conversionType == 'a' || conversionType == 'A') { startIndex += 2; } return padding(result, startIndex); } private static boolean startsWithMinusSign(CharSequence cs, String minusSign) { if (cs.length() < minusSign.length()) { return false; } for (int i = 0; i < minusSign.length(); ++i) { if (minusSign.charAt(i) != cs.charAt(i)) { return false; } } return true; } 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; } } }