Using printf with Customized Formattable Classes

by John Zukowski

Java SE 1.5 added the ability to format output using formatting strings like "%5.2f%n" to print a floating point number and a newline. An October 2004 tip titled Formatting Output with the New Formatter described this.

The Formattable interface is an important feature but wasn't part of the earlier tip. This interface is in the java.util package. When your class implements the Formattable interface, the Formatter class can use it to customize output formatting. You are no longer limited to what is printed by toString() for your class. By implementing the formatTo() method of Formattable, you can have your custom classes limit their output to a set width or precision, left or right justify the content, and even offer different output for different locales, just like the support for the predefined system data types.

The single formatTo() method of Formattable takes four arguments:

public void formatTo(Formatter formatter, int flags, int width, int precision)

The formatter argument represents the Formatter from which to get the locale and send the output when done.

The flags parameter is a bitmask of the FormattableFlags set. The user can have a - flag to specify left justified (LEFT_JUSTIFY), \^ flag for locale-sensitive uppercase (UPPERCASE), and # for using the alternate (ALTERNATE) formatting.

A width parameter represents the minimum output width, using spaces to fill the output if the displayed value is too short. The width value -1 means no minimum. If output is too short, output will be left justified if the flag is set. Otherwise, it is right justified.

A precision parameter specifies the maximum number of characters to output. If the output string is "1234567890" with a precision of 5 and a width of 10, the first five characters will be displayed, with the remaining five positions filled with spaces, defining a string of width 10. Having a precision of -1 means there is no limit.

A width or precision of -1 means no value was specified in the formatting string for that setting.

When creating a class to be used with printf and Formatter, you never call the formatTo() method yourself. Instead, you just implement the interface. Then, when your class is used with printf, the Formatter will call formatTo() for your class to find out how to display its value. To demonstrate, let us create some object that has both a short and long name that implements Formattable. Here's what the start of the class definition looks like. The class has only two properties, an empty implementation of Formattable, and its toString() method.

import java.util.Formattable;
import java.util.Formatter;

public class SomeObject implements Formattable {
    private String shortName;
    private String longName;

    public SomeObject(String shortName, String longName) {
        this.shortName = shortName;
        this.longName = longName;
    }

    public String getShortName() {
        return shortName;
    }

    public void setShortName(String shortName) {
        this.shortName = shortName;
    }

    public String getLongName() {
        return longName;
    }

    public void setLongName(String longName) {
        this.longName = longName;
    }

    public void formatTo(Formatter formatter, int flags,
        int width, int precision) {

    }

    public String toString() {
        return longName + " [" + shortName + "]";
    }
}

As it is now, printing the object with println() will display the long name, followed by the short name within square brackets as defined in the toString() method. Using the Formattable interface, you can improve the output. A better output will use the current property values and formattable flags. For this example, formatTo() will support the ALTERNATE and LEFT_JUSTIFY flags of FormattableFlags.

The first thing to do in formatTo() is to find out what to output. For SomeObject, the long name will be the default to display, and the short name will be used if the precision is less than 7 or if the ALTERNATE flag is set. Checking whether the ALTERNATE flag is set requires a typical bitwise flag check. Be careful with the -1 value for precision because that value means no limit. Check the range for the latter case. Then, pick the starting string based upon the settings.

String name = longName;
boolean alternate = 
    (flags & FormattableFlags.ALTERNATE) == FormattableFlags.ALTERNATE;
alternate |= (precision >= 0 && precision < 7);
String out = (alternate ? shortName : name);

Once you have the starting string, you get to shorten it down if necessary, based on the precision passed in. If the precision is unlimited or the string fits, just use that for the output. If it doesn't fit, then you need to trim it down. Typically, if something doesn't fit, the last character is replaced by a \*, which is done here.

StringBuilder sb = new StringBuilder();
if (precision == -1 || out.length() <= precision) {
    sb.append(out);
} else {
    sb.append(out.substring(0, precision - 1)).append('\*');
}

To demonstrate how to access the locale setting, the example here will reverse the output string for Chinese. More typically a translated starting string will be used based on the locale. For numeric output, the locale defines how decimals and commas appear within numbers.

if (formatter.locale().equals(Locale.CHINESE)) {
    sb.reverse();
}

Now that the output string is within a StringBuilder buffer, you can fill up the output buffer based upon the desired width and justification setting. For each position available within the desired width, add a space to beginning or end based upon the justification formattable flag.

int len = sb.length();
if (len < width) {
    boolean leftJustified = (flags & FormattableFlags.LEFT_JUSTIFY) 
        == FormattableFlags.LEFT_JUSTIFY;
    for (int i = 0; i < width - len; i++) {
        if (leftJustified) {
            sb.append(' ');
        } else {
            sb.insert(0, ' ');
        }
    }
}

The last thing to do is to send the output buffer to the Formatter. That's done by sending the whole String to the format() method of formatter:

formatter.format(sb.toString());

Add in some test cases, and that gives you the whole class definition, shown here:

import java.util.Formattable;
import java.util.FormattableFlags;
import java.util.Formatter;
import java.util.Locale;

public class SomeObject implements Formattable {
    private String shortName;
    private String longName;

    public SomeObject(String shortName, String longName) {
        this.shortName = shortName;
        this.longName = longName;
    }

    public String getShortName() {
        return shortName;
    }

    public void setShortName(String shortName) {
        this.shortName = shortName;
    }

    public String getLongName() {
        return longName;
    }

    public void setLongName(String longName) {
        this.longName = longName;
    }

    public void formatTo(Formatter formatter, int flags,
            int width, int precision) {
        StringBuilder sb = new StringBuilder();
        String name = longName;
        boolean alternate = (flags & FormattableFlags.ALTERNATE)
            == FormattableFlags.ALTERNATE;
        alternate |= (precision >= 0 && precision < 7); //
        String out = (alternate ? shortName : name);

        // Setup output string length based on precision
        if (precision == -1 || out.length() <= precision) {
            sb.append(out);
        } else {
            sb.append(out.substring(0, precision - 1)).append('\*');
        }

        if (formatter.locale().equals(Locale.CHINESE)) {
            sb.reverse();
        }

        // Setup output justification
        int len = sb.length();
        if (len < width) {
            boolean leftJustified =
                    (flags & FormattableFlags.LEFT_JUSTIFY) ==
                    FormattableFlags.LEFT_JUSTIFY;
            for (int i = 0; i < width - len; i++) {
                if (leftJustified) {
                    sb.append(' ');
                } else {
                    sb.insert(0, ' ');
                }
            }
        }
        formatter.format(sb.toString());
    }

    public String toString() {
        return longName + " [" + shortName + "]";
    }

    public static void main(String args[]) {
        SomeObject obj = new SomeObject("Short", "Somewhat longer name");
        System.out.printf(">%s<%n", obj);
        System.out.println(obj); // Regular obj.toString() call
        System.out.printf(">%#s<%n", obj);
        System.out.printf(">%.5s<%n", obj);
        System.out.printf(">%.8s<%n", obj);
        System.out.printf(">%-25s<%n", obj);
        System.out.printf(">%15.10s<%n", obj);
        System.out.printf(Locale.CHINESE, ">%15.10s<%n", obj);
    }
}

Running this program produces the following output:

>Somewhat longer name<
Somewhat longer name [Short]
>Short<
>Short<
>Somewha\*<
>Somewhat longer name     <
>     Somewhat \*<
>     \* tahwemoS<

The test program creates a SomeObject with a short name of "Short" and a long name of "Somewhat longer name". The first line here prints out the object's long name with the %s setting. The second outputs the object via the more typical toString(). The third line uses the alternate form. The next line doesn't explicitly ask for the alternate short form, but because the precision is so small, displays it anyways. Next, a precision is specified that is long enough to not use the alternate format, but too short to display the whole long name. Thus, a \* shows more characters are available. Next the longer name is displayed left justified. The final two show what happens when the width is wider than the precision, with one also showing the reversed "Chinese" version of the string.

That really is all there is to make your own classes work with printf. Whenever you want to display them, be sure to use a properly configured %s setting within the formatting string.

If you still have questions about using printf, be sure to visit the tip mentioned earlier, titled Formatting Output with the New Formatter.

For more information on the Formattable interface, see the documentation for the interface.

Comments:

Post a Comment:
Comments are closed for this entry.
About

John O'Conner

Search

Categories
Archives
« April 2014
SunMonTueWedThuFriSat
  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
   
       
Today