Table of Contents
If you’ve used C/C++ I/O in the past, it may have occurred to you that C’s printf is pretty convenient for formatting output. So you may have asked yourself: “Is there anything like printf in Java? How do I gain greater control of formatting my output?”
Before version 5 (1.5) of Java, it wasn’t easy. Java provided a set of classes to allow you to format messages, but they were (and still are) not the easiest things to use.
Fortunately, Java 5/1,5 added the ability to format messages in a way that is much more compatible with C/C++’s printf, that was also available in many other languages. It is much easier to use, and many people are already familiar with the principles of printf formatting.
The Format Family
For some time, Java has provided the ability to format messages. It uses the following classes to achieve this functionality:
FormatMessageFormatNumberFormatDecimalFormatChoiceFormat
DateFormat
These classes are part of the java.text package, which is primarily used for Internationalization. The subject of Internationalization (commonly called “I18N”) is a very complex one, and we won’t be covering it in detail here. Instead, we’ll just give some examples of how to do the equivalent of some printf operations (albeit in a more convoluted fashion).
MessageFormat
MessageFormat provides a means to produce concatenated messages in natural language-neutral way. You use this to construct messages displayed for end users. MessageFormat takes a set of objects, formats them, then inserts the formatted strings into the pattern at the appropriate places.
For example:
package inputOutput;
import java.text.MessageFormat;
import java.util.GregorianCalendar;
public class FormatText
{
public static void main(String[] args)
{
String pattern =
"On {2}, a {0} destroyed {1} houses and caused {3} of damage.";
MessageFormat msgFormat = new MessageFormat(pattern);
Object[] msgArgs =
{
"hurricane",
new Integer(99),
new GregorianCalendar(1999, 0, 1).getTime(),
new Double(10E7)
};
System.out.println(msgFormat.format(msgArgs));
}
}
which outputs:
On 1/1/99 12:00 AM, a hurricane destroyed 99 houses and caused 100,000,000 of damage.
Explicit Formats
We can change the way each object is displayed in the formatted string, by:
- Adding placeholder information to the string itself, or
- Calling the appropriate
setFormatorsetFormatsmethod
Using the setFormat method
Here’s how to do it by calling setFormat methods:
package inputOutput;
import java.text.MessageFormat;
import java.text.DateFormat;
import java.text.NumberFormat;
import java.util.GregorianCalendar;
public class FormatText
{
public static void main(String[] args)
{
String pattern =
"On {2}, a {0} destroyed {1} houses and caused {3} of damage.";
MessageFormat msgFormat = new MessageFormat(pattern);
msgFormat.setFormat(0, DateFormat.getDateInstance(DateFormat.LONG));
msgFormat.setFormat(3, NumberFormat.getCurrencyInstance());
Object[] msgArgs =
{
"hurricane",
new Integer(99),
new GregorianCalendar(1999, 0, 1).getTime(),
new Double(10E7)
};
System.out.println(msgFormat.format(msgArgs));
}
}
which produces:
On January 1, 1999, a hurricane destroyed 99 houses and caused $100,000,000.00 of damage.
Using the setFormats method
We could have accomplished the same thing as follows:
package inputOutput;
import java.text.MessageFormat;
import java.text.DateFormat;
import java.text.Format;
import java.text.NumberFormat;
import java.util.GregorianCalendar;
public class FormatText
{
public static void main(String[] args)
{
String pattern =
"On {2}, a {0} destroyed {1} houses and caused {3} of damage.";
MessageFormat msgFormat = new MessageFormat(pattern);
Format argFormats[] =
{
DateFormat.getDateInstance(DateFormat.LONG),
null,
null,
NumberFormat.getCurrencyInstance()
};
msgFormat.setFormats(argFormats);
Object[] msgArgs =
{
"hurricane",
new Integer(99),
new GregorianCalendar(1999, 0, 1).getTime(),
new Double(10E7)
};
System.out.println(msgFormat.format(msgArgs));
}
}
You can repeat the same argument, but with a different format:
package inputOutput;
import java.text.MessageFormat;
import java.text.DateFormat;
import java.text.Format;
import java.util.GregorianCalendar;
public class FormatText
{
public static void main(String[] args)
{
String pattern =
"On {2}, a {0} touched down at {2} and destroyed {1} houses.";
MessageFormat msgFormat = new MessageFormat(pattern);
Format argFormats[] =
{
DateFormat.getDateInstance(DateFormat.LONG),
null,
DateFormat.getTimeInstance(DateFormat.SHORT),
null
};
msgFormat.setFormats(argFormats);
Object[] msgArgs =
{
"tornado",
new Integer(99),
new GregorianCalendar(1999, 0, 1).getTime()
};
System.out.println(msgFormat.format(msgArgs));
}
}
which outputs:
On January 1, 1999, a tornado touched down at 12:00 AM and destroyed 99 houses.
Note: The argument formats correspond to the position of the placeholder, while the message arguments correspond to the number of the placeholder.
Specifying Formats in the Placeholder
Another way of controlling the format is to specify the formats in the placeholder itself:
package inputOutput;
import java.text.MessageFormat;
import java.util.GregorianCalendar;
public class FormatText
{
public static void main(String[] args)
{
String pattern =
"On {2,date,long}, a {0} touched down at {2,time,short}, " +
"destroyed {1} houses, and caused {3,number,currency} of damage.";
MessageFormat msgFormat = new MessageFormat(pattern);
Object[] msgArgs =
{
"tornado",
new Integer(99),
new GregorianCalendar(1999, 0, 1).getTime(),
new Double(10E7)
};
System.out.println(msgFormat.format(msgArgs));
}
}
which produces:
On January 1, 1999, a tornado touched down at 12:00 AM, destroyed 99 houses, and caused $100,000,000.00 of damage.
Choice Formats
Consider the following modification of the above program:
package inputOutput;
import java.text.MessageFormat;
import java.util.GregorianCalendar;
public class FormatText
{
public static void main(String[] args)
{
String pattern =
"On {2,date,long}, a {0} " +
"destroyed {1} houses, and caused {3,number,currency} of damage.";
MessageFormat msgFormat = new MessageFormat(pattern);
Object[] msgArgs =
{
"earthquake",
new Integer(1),
new GregorianCalendar(1999, 0, 1).getTime(),
new Double(10E7)
};
System.out.println(msgFormat.format(msgArgs));
}
}
This outputs the string:
On January 1, 1999, a earthquake destroyed 1 houses, and caused $100,000,000.00 of damage.
which should, to be grammatically correct, read:
On January 1, 1999, an earthquake destroyed 1 house, and caused $100,000,000.00 of damage.
In other words, we have to deal with plurals correctly, as well as ensuring that parts of speech match properly (such as indefinite articles, etc.). This can be more important and more complex in other languages such as German or French, where articles must match the gender and context of a noun.
ChoiceFormat was designed to let you solve this problem. You must specify:
- An array of limits
- An array of format strings
Here’s an example of how to solve the above problem:
package inputOutput;
import java.text.ChoiceFormat;
import java.text.MessageFormat;
import java.util.GregorianCalendar;
public class FormatText
{
public static void main(String[] args)
{
double[] limits = {1, 1, 2};
String[] formatStrings =
{"no houses", "one house",
"{1,number,integer} houses"};
ChoiceFormat choiceFormat =
new ChoiceFormat(limits, formatStrings);
String pattern =
"On {2,date,long}, {0} " +
"destroyed {1}, and caused {3,number,currency} of damage.";
MessageFormat msgFormat = new MessageFormat(pattern);
msgFormat.setFormat(2, choiceFormat);
Object[] msgArgs =
{
"an earthquake",
new Integer(1),
new GregorianCalendar(1999, 0, 1).getTime(),
new Double(10E7)
};
System.out.println(msgFormat.format(msgArgs));
}
}
which outputs:
On January 1, 1999, an earthquake destroyed one house, and caused $100,000,000.00 of damage.
Or, we can embed the same choices into the placeholders themselves:
package inputOutput;
import java.text.MessageFormat;
import java.util.GregorianCalendar;
public class FormatText
{
public static void main(String[] args)
{
String pattern =
"On {2,date,long}, {0} destroyed " +
"{1,choice,0#no houses|1#one house|2#{1,number,integer} houses}, " +
"and caused {3,number,currency} of damage.";
MessageFormat msgFormat = new MessageFormat(pattern);
Object[] msgArgs =
{
"an earthquake",
new Integer(1),
new GregorianCalendar(1999, 0, 1).getTime(),
new Double(10E7)
};
System.out.println(msgFormat.format(msgArgs));
}
}
This is actually far preferred, because it means that we can remove the message(s) from the program, and provide separate sets of messages for each language/locale, and have the separated messages contain all the necessary instructions to format the messages correctly in each language. That way, the program does not have special code for each language.
Believe me, this is very important when you are writing code that will have to be internationalized (globalization is here, already!)
Summary of Patterns
Summary of MessageFormat Patterns
Here is the grammar for MessageFormat Patterns:
messageFormatPattern := string ( "{" messageFormatElement "}" string )*
messageFormatElement := argument { "," elementFormat }
elementFormat := "time" { "," datetimeStyle }
| "date" { "," datetimeStyle }
| "number" { "," numberStyle }
| "choice" { "," choiceStyle }
datetimeStyle := "short"
| "medium"
| "long"
| "full"
| dateFormatPattern
numberStyle := "currency"
| "percent"
| "integer"
| numberFormatPattern
choiceStyle := choiceFormatPattern
Notes:
- If there is no
elementFormat, then the argument must be aString, which is substituted. - If there is no
dateTimeStyleornumberStyle, then the default format is used (for example,NumberFormat.getInstance,DateFormat.getTimeInstance, orDateFormat.getInstance). - In strings, single quotes can be used to quote the “
{” (curly brace) if necessary. A real single quote is represented by''(two successive single quotes). Inside amessageFormatElement, quotes are not removed. For example,{1,number,$'#',##}will produce a number format with the pound-sign quoted, with a result such as: “$#31,45“. - If a pattern is used, then unquoted braces in the pattern, if any, must match: that is, “
ab {0} de“ and “ab '}' de“ are OK, but “ab {0'}' de“ and “ab } de“ are not. - The argument is a number from
0to9, which corresponds to the arguments presented in an array to be formatted. - It is ok to have unused arguments in the array. With missing arguments or arguments that are not of the right class for the specified format, a
ParseExceptionis thrown. First,formatchecks to see if aFormatobject has been specified for the argument with thesetFormatsmethod. If so, thenformatuses that Format object to format the argument. Otherwise, the argument is formatted based on the object’s type:- If the argument is a
Number, then format usesNumberFormat.getInstanceto format the argument; - if the argument is a
Date, then format usesDateFormat.getDateTimeInstanceto format the argument. - Otherwise, it uses the
toStringmethod.
- If the argument is a
Summary of ChoiceFormat
A ChoiceFormat allows you to attach a format to a range of numbers. It is generally used in a MessageFormat for handling plurals. The choice is specified with an ascending list of doubles, where each item specifies a half-open interval up to the next item:
X matches j if and only if limit[j] <= X < limit[j+1]
If there is no match, then either the first or last index is used, depending on whether the number (X) is too low or too high.
Formatting Numbers
When you use MessageFormat and provide a number as one of the arguments, you must specify an object, not a primitive type.
The Number class hierarchy is as follows:
- Number
- Byte
- Double
- Float
- Integer
- Long
- Short
- BigInteger
- BigDecimal
The family of classes used to format numbers is:
FormatNumberFormatDecimalFormatChoiceFormat
Using NumberFormat
NumberFormat is the abstract base class for all number formats. This class provides the interface for formatting and parsing numbers. NumberFormat also provides methods for determining which locales have number formats, and what their names are. NumberFormat helps you to format and parse numbers for any locale. Your code can be completely independent of the locale conventions for decimal points, thousands-separators, or even the particular decimal digits used, or whether the number format is even decimal.
To format a number for the current Locale, use one of the factory class methods:
myString = NumberFormat.getInstance().format(myNumber);
If you are formatting multiple numbers, it is more efficient to get the format and use it multiple times so that the system doesn’t have to fetch the information about the local language and country conventions multiple times.
NumberFormat nf = NumberFormat.getInstance();
for (int i = 0; i < a.length; ++i)
{
output.println(nf.format(myNumber[i]) + "; ");
}
To format a number for a different Locale, specify it in the call to getInstance.
NumberFormat nf = NumberFormat.getInstance(Locale.FRENCH);
You can also use a NumberFormat to parse numbers:
myNumber = nf.parse(myString);
Use getInstance or getNumberInstance to get the normal number format. Use getCurrencyInstance to get the currency number format. And use getPercentInstance to get a format for displaying percentages. With this format, a fraction like 0.53 is displayed as 53%.
You can also control the display of numbers with such methods as setMinimumFractionDigits. If you want even more control over the format or parsing, or want to give your users more control, you can try casting the NumberFormat you get from the factory methods to a DecimalFormat. This will work for the vast majority of locales; just remember to put it in a try block in case you encounter an unusual one.
NumberFormat and DecimalFormat are designed such that some controls work for formatting and others work for parsing. The following is the detailed description for each these control methods,
setParseIntegerOnly: only affects parsing, e.g. if true, “3456.78” ->3456(and leaves the parse position just after index6) if false, “3456.78” ->3456.78(and leaves the parse position just after index8) This is independent of formatting. If you want to not show a decimal point where there might be no digits after the decimal point, usesetDecimalSeparatorAlwaysShown.setDecimalSeparatorAlwaysShown: only affects formatting, and only where there might be no digits after the decimal point, such as with a pattern like “#,##0.##“, e.g., if true,3456.00-> “3,456.” if false,3456.00-> “3456” This is independent of parsing. If you want parsing to stop at the decimal point, usesetParseIntegerOnly.
You can also use forms of the parse and format methods with ParsePosition and FieldPosition to allow you to:
- progressively parse through pieces of a string
- align the decimal point and other areas For example, you can align numbers in two ways:
- If you are using a monospaced font with spacing for alignment, you can pass the
FieldPositionin yourformatcall, withfield = INTEGER_FIELD. On output,getEndIndexwill be set to the offset between the last character of the integer and the decimal. Add (desiredSpaceCount - getEndIndex) spaces at the front of the string. - If you are using proportional fonts, instead of padding with spaces, measure the width of the string in pixels from the start to
getEndIndex. Then move the pen by (desiredPixelWidth - widthToAlignmentPoint) before drawing the text. It also works where there is no decimal, but possibly additional characters at the end, e.g. with parentheses in negative numbers: “(12)” for-12.
- If you are using a monospaced font with spacing for alignment, you can pass the
Using DecimalFormat
DecimalFormat is a concrete subclass of NumberFormat for formatting decimal numbers. This class allows for a variety of parameters, and localization to Western, Arabic, or Indic numbers. Normally, you get the proper NumberFormat for a specific locale (including the default locale) using one of NumberFormat‘s factory methods such as getInstance. You may then modify it from there (after testing to make sure it is a DecimalFormat, of course!)
Special cases:
- NaN (IEEE floating point “not-a-number”) is formatted as a single character, typically
\uFFFD. - +/- Infinity is formatted as a single character, typically
\u221E, plus the positive and negative pre/suffixes.
Note: this class is designed for common users; for very large or small numbers, use a format that can express exponential values.
The following shows the structure of the pattern:
pattern := subpattern{;subpattern}
subpattern := {prefix}integer{.fraction}{suffix}
prefix := '\\u0000'..'\\uFFFD' - specialCharacters
suffix := '\\u0000'..'\\uFFFD' - specialCharacters
integer := '#'* '0'* '0'
fraction := '0'* '#'*
Notation:
X* 0 or more instances of X
(X | Y) either X or Y.
X..Y any character from X up to Y, inclusive.
S - T characters in S, except those in T
The first subpattern is for positive numbers. The second (optional) subpattern is for negative numbers. (In both cases, ‘,‘ can occur inside the integer portion — it is just too messy to indicate in BNF — Backus–Naur form, used above.)
Here are the special characters used in the parts of the subpattern:
| Symbol | Meaning |
|---|---|
| 0 | a digit |
| # | a digit, zero shows as absent |
| . | placeholder for decimal separator |
| , | placeholder for grouping separator. |
| ; | separates formats. |
| – | default negative prefix. |
| % | multiply by 100 and show as percentage |
| ‰ | multiply by 1000 and show as per mille |
| ¤ | currency sign; replaced by currency symbol; if doubled, replaced by international currency symbol. If present in a pattern, the monetary decimal separator is used instead of the decimal separator. |
| X | any other characters can be used in the prefix or suffix |
| ‘ | used to quote special characters in a prefix or suffix. |
Here are some examples of typical format string usage:
| Format String | Meaning | Value | Output String |
|---|---|---|---|
| “##0.00” | At least one digit before the decimal point, and two after. | 1234.567 | 1234.56 |
| “#.000” | Possibly no digits before the point, three after. | 1234.567 | 1234.567 |
| “,###” | Use thousands separator and no decimal places. | 1234.567 | 1,234 |
| “0.00;0.00-“ | Show negative numbers with sign on the right. | -27.5 | 27.5- |
Notes
- If there is no explicit negative subpattern, – is prefixed to the positive form. That is, “
0.00” alone is equivalent to “0.00;-0.00“. - Illegal patterns, such as “
#.#.#” or mixing ‘_‘ and ‘*‘ in the same pattern, will cause anIllegalArgumentExceptionto be thrown. From the message ofIllegalArgumentException, you can find the place in the string where the error occurred. - The grouping separator is commonly used for thousands, but in some countries for ten-thousands. The interval is a constant number of digits between the grouping characters, such as
100,000,000or1,0000,0000. If you supply a pattern with multiple grouping characters, the interval between the last one and the end of the integer is the one that is used. So “#,##,###,####” == “######,####” == “##,####,####“. - When calling
DecimalFormat.parse(String, ParsePosition)and parsing fails, a null object will be returned. The unchanged parse position also reflects that an error has occurred during parsing. When calling the convenient methodDecimalFormat.parse(String)and parsing fails, aParseExceptionwill be thrown. - This class only handles localized digits where the 10 digits are contiguous in Unicode, from 0 to 9. Other digits sets (such as superscripts) would need a different subclass.
DecimalFormat Example
Here’s an example using DecimalFormat:
package inputOutput;
import java.util.Locale;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.text.ParseException;
public class FormatDecimal
{
public static void main(String[] args)
{
// Gather the available locales
Locale[] locales = NumberFormat.getAvailableLocales();
double myNumber = -1234.56;
NumberFormat form;
// Print out a number with the locale number, currency
// and percent format for each locale we can.
for (int j = 0; j < 3; ++j)
{
System.out.println("FORMAT");
for (int i = 0; i < locales.length; ++i)
{
if (locales[i].getCountry().length() == 0)
{
// skip language-only
continue;
}
System.out.print(locales[i].getDisplayName());
switch (j)
{
default:
form = NumberFormat.getInstance(locales[i]);
break;
case 1:
form = NumberFormat.getCurrencyInstance(locales[i]);
break;
case 0:
form = NumberFormat.getPercentInstance(locales[i]);
break;
}
try
{
System.out.print(": " +
((DecimalFormat)form).toPattern() +
" -> " + form.format(myNumber));
}
catch (IllegalArgumentException iae)
{ }
try
{
System.out.println(" -> " +
form.parse(
form.format(myNumber)));
}
catch (ParseException pe)
{ }
}
}
}
}
which, on my Microsoft Windows system, prints out a very long list:
FORMAT Arabic (Egypt): #,##0% -> -123,456% -> -1234.56 Byelorussian (Belarus): #,##0% -> -123 456% -> -1234.56 Bulgarian (Bulgaria): #,##0% -> -123 456% -> -1234.56 Catalan (Spain): #,##0% -> -123.456% -> -1234.56 Czech (Czech Republic): #,##0% -> -123.456% -> -1234.56 Danish (Denmark): #,##0% -> -123.456% -> -1234.56 German (Germany): #,##0% -> -123.456% -> -1234.56 German (Germany,Euro): #,##0% -> -123.456% -> -1234.56 German (Austria): #,##0% -> -123.456% -> -1234.56 German (Austria,Euro): #,##0% -> -123.456% -> -1234.56 German (Switzerland): #,##0% -> -123'456% -> -1234.56 German (Luxembourg): #,##0% -> -123.456% -> -1234.56 German (Luxembourg,Euro): #,##0% -> -123.456% -> -1234.56 Greek (Greece): #,##0% -> -123.456% -> -1234.56 English (United States): #,##0% -> -123,456% -> -1234.56 English (Australia): #,##0% -> -123,456% -> -1234.56 English (Canada): #,##0% -> -123,456% -> -1234.56 English (United Kingdom): #,##0% -> -123,456% -> -1234.56 English (Ireland): #,##0% -> -123,456% -> -1234.56 ... Japanese (Japan): #,##0.### -> -1,234.56 -> -1234.56 Korean (South Korea): #,##0.### -> -1,234.56 -> -1234.56 Lithuanian (Lithuania): #,##0.## -> -1.234,56 -> -1234.56 Latvian (Lettish) (Latvia): #,##0.### -> -1 234,56 -> -1234.56 Macedonian (Macedonia): #,##0.###;(#,##0.###) -> (1.234,56) -> -1234.56 Dutch (Netherlands): #,##0.### -> -1.234,56 -> -1234.56 Dutch (Netherlands,Euro): #,##0.### -> -1.234,56 -> -1234.56 Dutch (Belgium): #,##0.### -> -1.234,56 -> -1234.56 Dutch (Belgium,Euro): #,##0.### -> -1.234,56 -> -1234.56 Norwegian (Norway,B): #,##0.### -> -1 234,56 -> -1234.56 Norwegian (Norway,NY): #,##0.### -> -1 234,56 -> -1234.56 Polish (Poland): #,##0.### -> -1 234,56 -> -1234.56 Portuguese (Portugal): #,##0.### -> -1.234,56 -> -1234.56 Portuguese (Portugal,Euro): #,##0.### -> -1.234,56 -> -1234.56 Portuguese (Brazil): #,##0.### -> -1.234,56 -> -1234.56 Romanian (Romania): #,##0.### -> -1.234,56 -> -1234.56 Russian (Russian Federation): #,##0.### -> -1 234,56 -> -1234.56 Serbo-Croatian (Yugoslavia): #,##0.### -> -1.234,56 -> -1234.56 Slovak (Slovakia): #,##0.### -> -1 234,56 -> -1234.56 Slovenian (Slovenia): #,##0.### -> -1.234,56 -> -1234.56 Albanian (Albania): #,##0.### -> -1.234,56 -> -1234.56 Serbian (Yugoslavia): #,##0.### -> -1 234,56 -> -1234.56 Swedish (Sweden): #,##0.### -> -1 234,56 -> -1234.56 Thai (Thailand): #,##0.### -> -1,234.56 -> -1234.56 Turkish (Turkey): #,##0.### -> -1.234,56 -> -1234.56 Ukrainian (Ukraine): #,##0.### -> -1.234,56 -> -1234.56 Chinese (China): #,##0.### -> -1,234.56 -> -1234.56 Chinese (Taiwan): #,##0.### -> -1,234.56 -> -1234.56
Using MessageFormat
However, for most purposes, you can use MessageFormat to format numbers:
package inputOutput;
import java.text.MessageFormat;
public class FormatNumber
{
public static void main(String[] args)
{
Customer[] cust =
{
new Customer(1, "Heather Gilmartin", 200.00),
new Customer(2, "Bill Gates", -3000000.00),
new Customer(500, "Crocodile Dundee", 1000.00)
};
for (int i = 0; i < cust.length; i++)
{
printCustomerStatus(cust[i]);
}
}
static void printCustomerStatus(Customer cust)
{
String pattern =
"Customer {0,number,000}, {1}, has a balance of " +
"{2,number,¤###,##0.00;(¤###,##0.00)}"; //}";
MessageFormat msgFormat = new MessageFormat(pattern);
Object[] msgArgs =
{
new Integer(cust.getId()),
cust.getName(),
new Double(cust.getBalance())
};
System.out.println(msgFormat.format(msgArgs));
}
static class Customer
{
Customer(int id, String name, double balance)
{
m_id = id;
m_name = name;
m_balance = balance;
}
int getId() { return m_id; }
String getName() { return m_name; }
double getBalance(){ return m_balance; }
private int m_id;
private String m_name;
private double m_balance;
}
}
which produces the following output:
Customer 001, Heather Gilmartin, has a balance of $200.00 Customer 002, Bill Gates, has a balance of ($3,000,000.00) Customer 500, Crocodile Dundee, has a balance of $1,000.00
The printf Family
In Java 5, they finally added a printf functionality similar to that of C/C++ and other languages. It was difficult to do that before Java 5 because there was no support for variable-length argument lists — they added support for that in Java 5.
So, here’s how printf and family work…
Classes Involved
Here are the players in this new printf game:
The class java.io.PrintStream has added new methods: | public PrintStream printf(String format, Object… args)public PrintStream printf(Locale l, String format, Object… args)public PrintStream format(String format, Object… args)public PrintStream format(Locale l, String format, Object… args) |
The class java.io.PrintWriter has added new methods: | public PrintWriter printf(String format, Object… args)public PrintWriter printf(Locale l, String format, Object… args)public PrintWriter format(String format, Object… args)public PrintWriter format(Locale l, String format, Object… args) |
The class java.lang.String has added new methods: | public static String format(String format, Object… args)public static String format(Locale l, String format, Object… args) |
The work is actually performed by class java.util.Formatter, which is documented (for Java 5.0) here. Almost the entire javadoc for class Formatter is the specification for how such formatting is supposed to work, so it’s worth a close read.
Notice that each of the above methods employs the new (as of Java 5/1.5) variable number of arguments syntax.
In each of the above methods, the parameter, format, is known as a Format String, which may contain zero or more Format Specifiers.
What’s a Format Specifier? Read on…
Format Specifiers
What’s a Format Specifier? Here’s the scoop:
A call to one of the methods in the printf family involves passing in a Format String, which contains:
- Normal text
plus
- Zero or more Format Specifiers
In most cases, a Format Specifier is associated with one of the arguments passed in after the Format String argument. A Format Specifier, as its name suggests, specifies the output format for how that argument’s value should be displayed.
General, Character and Numeric Type Format Specifiers
The format specifiers for general, character, and numeric types have the following syntax:
%[argument_index$][flags][width][.precision]conversion
The optional argument_index is a decimal integer indicating the position of the argument in the argument list. The first argument is referenced by “1$”, the second by “2$”, etc.
The optional flags is a set of characters that modify the output format. The set of valid flags depends on the conversion.
The optional width is a non-negative decimal integer indicating the minimum number of characters to be written to the output.
The optional precision is a non-negative decimal integer usually used to restrict the number of characters. The specific behavior depends on the conversion.
The required conversion is a character indicating how the argument should be formatted. The set of valid conversions for a given argument depends on the argument’s data type.
Date and Time Format Specifiers
The format specifiers for types which are used to represents dates and times have the following syntax:
%[argument_index$][flags][width]conversion
The optional argument_index, flags and width are defined as above.
The required conversion is a two character sequence. The first character is ‘t’ or ‘T’. The second character indicates the format to be used. These characters are similar to but not completely identical to those defined by GNU date and POSIX strftime(3c).
Non-Argument Format Specifiers
The format specifiers which do not correspond to arguments have the following syntax:
%[flags][width]conversion
The optional flags and width is defined as above.
The required conversion is a character indicating content to be inserted in the output.
Some Examples
Here are some examples of how to use printf and family.
We start out simple, and build up…
Using Plain print & println
First, let’s take a look at what we get if we just use System.out.print[ln] calls.
For example, let’s produce a table from some employee data.
Our first attempt will be pretty crude:
package inputOutput;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* Class to produce a table of information about customers
*/
public class EmployeeTablePrinter1
{
public static void main(String[] args)
{
List<Employee1> employees = new ArrayList<Employee1>();
// Populate list with Employees
employees.add( new Employee1(1, "Bloggs, Fred", "10 Firebird Crescent",
new Date(1976 - 1900, 5 - 1, 20), 40000.00 ) );
employees.add( new Employee1(2, "Charles, Cheryl", "5 Lattice Lane",
new Date(1989 - 1900, 4 - 1, 3), 50000.00 ) );
employees.add( new Employee1(3, "Romney, Titus", "42 St. Martins Rd.",
new Date(1973 - 1900, 11 - 1, 19), 45000.00 ) );
employees.add( new Employee1(4, "Clinton, Burton", "567 Nebula Circle",
new Date(1964 - 1900, 1 - 1, 29), 100000.00 ) );
employees.add( new Employee1(5, "McDuff, Hazel", "1 Church Row",
new Date(1985 - 1900, 3 - 1, 6), 55000.00 ) );
employees.add( new Employee1(6, "Frazier, Bill", "92 S. Main St.",
new Date(1980 - 1900, 12 - 1, 5), 70000.00 ) );
employees.add( new Employee1(7, "Zambroski, Heather", "589 Trumpington Place",
new Date(1959 - 1900, 4 - 1, 3), 50000.00 ) );
// Print out the table of employees
Employee1.printHeader();
for (Employee1 emp : employees)
{
emp.print();
}
}
}
/**
* Class to represent an employee
*/
class Employee1
{
Employee1(int id, String name, String address, Date dob, double salary)
{
m_id = id;
m_name = name;
m_address = address;
m_dob = dob;
m_salary = salary;
}
static void printHeader()
{
System.out.print("|");
System.out.print("ID" + "|");
System.out.print("Name" + "|");
System.out.print("Address" + "|");
System.out.print("Date of Birth" + "|");
System.out.println("Salary" + "|"); }
void print()
{
System.out.print("|");
System.out.print(m_id + "|");
System.out.print(m_name + "|");
System.out.print(m_address + "|");
System.out.print(m_dob + "|");
System.out.println(m_salary + "|");
}
private int m_id;
private String m_name;
private String m_address;
private Date m_dob;
private double m_salary;
}
Notes:
- I have used the Date class. The particular constructor for Date that I used is officially deprecated. However, I am using it because it is the simplest form to use for this example (Calendar, and GregorianCalendar are far too complex!)
- Other annoyances with the Date class include:
- The year must be specified from a base of 1900
- The month number is zero-based
When run, this program produces the following output:
|ID|Name|Address|Date of Birth|Salary| |1|Bloggs, Fred|10 Firebird Crescent|Thu May 20 00:00:00 EDT 1976|40000.0| |2|Charles, Cheryl|5 Lattice Lane|Mon Apr 03 00:00:00 EDT 1989|50000.0| |3|Romney, Titus|42 St. Martins Rd.|Mon Nov 19 00:00:00 EST 1973|45000.0| |4|Clinton, Burton|567 Nebula Circle|Wed Jan 29 00:00:00 EST 1964|100000.0| |5|McDuff, Hazel|1 Church Row|Wed Mar 06 00:00:00 EST 1985|55000.0| |6|Frazier, Bill|92 S. Main St.|Fri Dec 05 00:00:00 EST 1980|70000.0| |7|Zambroski, Heather|589 Trumpington Place|Fri Apr 03 00:00:00 EST 1959|50000.0|
Not exactly the best format one could hope for, right?
Fixing Some Problems
What are the problems? Well, here are some:
- The most obvious is that the lines that are intended to separate the data items don’t line up.
- The default formatting of the date of birth is likely to be gobbledygook to most people. Besides, we don’t want the time of birth in there!
- Conventionally, monetary amounts, like salary, are presented with a fixed, two digit, size for cents (the fractional part). Also, wouldn’t we like to know about monetary units? Is the value in dollars or euros or pounds, or what? And wouldn’t it be nicer to use commas, so we can read it more easily ($40,000.00, for example)?
- Perhaps we would expect an ID number to include some number of leading zeros — say, 0001, instead of simply 1. It’s conventional to make an ID be of fixed size by inserting those leading zeros.
Let’s see if we can fix the last three of these, by switching to using some printf features:
package inputOutput;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* Class to produce a table of information about customers
*/
public class EmployeeTablePrinter2
{
public static void main(String[] args)
{
List<Employee2> employees = new ArrayList<Employee2>();
// Populate list with Employees
employees.add( new Employee2(1, "Bloggs, Fred", "10 Firebird Crescent",
new Date(1976 - 1900, 5 - 1, 20), 40000.00 ) );
employees.add( new Employee2(2, "Charles, Cheryl", "5 Lattice Lane",
new Date(1989 - 1900, 4 - 1, 3), 50000.00 ) );
employees.add( new Employee2(3, "Romney, Titus", "42 St. Martins Rd.",
new Date(1973 - 1900, 11 - 1, 19), 45000.00 ) );
employees.add( new Employee2(4, "Clinton, Burton", "567 Nebula Circle",
new Date(1964 - 1900, 1 - 1, 29), 100000.00 ) );
employees.add( new Employee2(5, "McDuff, Hazel", "1 Church Row",
new Date(1985 - 1900, 3 - 1, 6), 55000.00 ) );
employees.add( new Employee2(6, "Frazier, Bill", "92 S. Main St.",
new Date(1980 - 1900, 12 - 1, 5), 70000.00 ) );
employees.add( new Employee2(7, "Zambroski, Heather", "589 Trumpington Place",
new Date(1959 - 1900, 4 - 1, 3), 50000.00 ) );
// Print out the table of employees
Employee2.printHeader();
for (Employee2 emp : employees)
{
emp.print();
}
}
}
/**
* Class to represent an employee
*/
class Employee2
{
Employee2(int id, String name, String address, Date dob, double salary)
{
m_id = id;
m_name = name;
m_address = address;
m_dob = dob;
m_salary = salary;
}
static void printHeader()
{
System.out.printf("|ID|Name|Address|Date of Birth|Salary|\n");
}
void print()
{
System.out.printf("|%04d|%s|%s|%tF|$%,.2f|\n",
m_id, m_name, m_address, m_dob, m_salary);
}
private int m_id;
private String m_name;
private String m_address;
private Date m_dob;
private double m_salary;
}
which now produces the following:
|ID|Name|Address|Date of Birth|Salary| |0001|Bloggs, Fred|10 Firebird Crescent|1976-05-20|$40,000.00| |0002|Charles, Cheryl|5 Lattice Lane|1989-04-03|$50,000.00| |0003|Romney, Titus|42 St. Martins Rd.|1973-11-19|$45,000.00| |0004|Clinton, Burton|567 Nebula Circle|1964-01-29|$100,000.00| |0005|McDuff, Hazel|1 Church Row|1985-03-06|$55,000.00| |0006|Frazier, Bill|92 S. Main St.|1980-12-05|$70,000.00| |0007|Zambroski, Heather|589 Trumpington Place|1959-04-03|$50,000.00|
Notes:
- The ID values are all now 4 characters wide, with leading zeros
- The date format is more readable (I chose ISO date format —
YYYY-MM-DD— because US date formatting conventions conflict with those used elsewhere in the world. But you can always change it to your own taste.) - The salary now specifies that it is in dollars, and always shows two decimal places and is comma-separated.
Width, Spacing & Justification
Let’s finally fix those spacing and alignment problems.
Here are my final changes:
package inputOutput;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* Class to produce a table of information about customers
*/
public class EmployeeTablePrinter3
{
public static void main(String[] args)
{
List<Employee3> employees = new ArrayList<Employee3>();
// Populate list with Employees
employees.add( new Employee3(1, "Bloggs, Fred", "10 Firebird Crescent",
new Date(1976 - 1900, 5 - 1, 20), 40000.00 ) );
employees.add( new Employee3(2, "Charles, Cheryl", "5 Lattice Lane",
new Date(1989 - 1900, 4 - 1, 3), 50000.00 ) );
employees.add( new Employee3(3, "Romney, Titus", "42 St. Martins Rd.",
new Date(1973 - 1900, 11 - 1, 19), 45000.00 ) );
employees.add( new Employee3(4, "Clinton, Burton", "567 Nebula Circle",
new Date(1964 - 1900, 1 - 1, 29), 100000.00 ) );
employees.add( new Employee3(5, "McDuff, Hazel", "1 Church Row",
new Date(1985 - 1900, 3 - 1, 6), 55000.00 ) );
employees.add( new Employee3(6, "Frazier, Bill", "92 S. Main St.",
new Date(1980 - 1900, 12 - 1, 5), 70000.00 ) );
employees.add( new Employee3(7, "Zambroski, Heather", "589 Trumpington Place",
new Date(1959 - 1900, 4 - 1, 3), 50000.00 ) );
// Print out the table of employees
Employee3.printHeader();
for (Employee3 emp : employees)
{
emp.print();
}
}
}
/**
* Class to represent an employee
*/
class Employee3
{
Employee3(int id, String name, String address, Date dob, double salary)
{
m_id = id;
m_name = name;
m_address = address;
m_dob = dob;
m_salary = salary;
}
static void printHeader()
{
System.out.println(ROW_SEPARATOR);
System.out.printf("| %-4s | %-20s | %-25s | %-14s | %-13s |\n",
"ID", "Name", "Address", "Date of Birth", "Salary");
System.out.println(ROW_SEPARATOR);
}
void print()
{
System.out.printf("| %04d | %-20s | %-25s | %-14tF | $%,12.2f |\n",
m_id, m_name, m_address, m_dob, m_salary);
System.out.println(ROW_SEPARATOR);
}
private int m_id;
private String m_name;
private String m_address;
private Date m_dob;
private double m_salary;
static private String ROW_SEPARATOR =
"+------+----------------------+---------------------------+----------------+---------------+";
}
which produces the following output:
+------+----------------------+---------------------------+----------------+---------------+ | ID | Name | Address | Date of Birth | Salary | +------+----------------------+---------------------------+----------------+---------------+ | 0001 | Bloggs, Fred | 10 Firebird Crescent | 1976-05-20 | $ 40,000.00 | +------+----------------------+---------------------------+----------------+---------------+ | 0002 | Charles, Cheryl | 5 Lattice Lane | 1989-04-03 | $ 50,000.00 | +------+----------------------+---------------------------+----------------+---------------+ | 0003 | Romney, Titus | 42 St. Martins Rd. | 1973-11-19 | $ 45,000.00 | +------+----------------------+---------------------------+----------------+---------------+ | 0004 | Clinton, Burton | 567 Nebula Circle | 1964-01-29 | $ 100,000.00 | +------+----------------------+---------------------------+----------------+---------------+ | 0005 | McDuff, Hazel | 1 Church Row | 1985-03-06 | $ 55,000.00 | +------+----------------------+---------------------------+----------------+---------------+ | 0006 | Frazier, Bill | 92 S. Main St. | 1980-12-05 | $ 70,000.00 | +------+----------------------+---------------------------+----------------+---------------+ | 0007 | Zambroski, Heather | 589 Trumpington Place | 1959-04-03 | $ 50,000.00 | +------+----------------------+---------------------------+----------------+---------------+
While far from ideal, it certainly beats our original, crude attempt!
Notes:
- I added a single space on either side of the data item to prevent it from being forced up against the column separator.
- I set minimum widths for the fields, to allow each column to be a standard width, including the header.
- I forced left alignment on all the header items, the names, addresses and dates of birth. The default alignment is right aligned, and that is most natural for the salary column, where you want the decimal points to align correctly.
- Note however, that right alignment causes a slight problem with the salary format: it causes the dollar sign to be separated from the amount. If you wanted to cause the dollar sign to be right up against the amount, then we could do that, but we’d have to do two levels of formatting.
