11

Using the example given for java.util.Formattable (modified to actually set values in the constructor), things seem to work mostly correctly:

import java.nio.CharBuffer;
import java.util.Formatter;
import java.util.Formattable;
import java.util.Locale;
import static java.util.FormattableFlags.*;

public class StockName implements Formattable {
    private String symbol, companyName, frenchCompanyName;
    public StockName(String symbol, String companyName,
                     String frenchCompanyName) {
        this.symbol = symbol;
        this.companyName = companyName;
        this.frenchCompanyName = frenchCompanyName;
    }

    public void formatTo(Formatter fmt, int f, int width, int precision) {
        StringBuilder sb = new StringBuilder();

        // decide form of name
        String name = companyName;
        if (fmt.locale().equals(Locale.FRANCE))
            name = frenchCompanyName;
        boolean alternate = (f & ALTERNATE) == ALTERNATE;
        boolean usesymbol = alternate || (precision != -1 && precision < 10);
        String out = (usesymbol ? symbol : name);

        // apply precision
        if (precision == -1 || out.length() < precision) {
            // write it all
            sb.append(out);
        } else {
            sb.append(out.substring(0, precision - 1)).append('*');
        }

        // apply width and justification
        int len = sb.length();
        if (len < width)
            for (int i = 0; i < width - len; i++)
                if ((f & LEFT_JUSTIFY) == LEFT_JUSTIFY)
                    sb.append(' ');
                else
                    sb.insert(0, ' ');

        fmt.format(sb.toString());
    }

    public String toString() {
        return String.format("%s - %s", symbol, companyName);
    }
}

Running

System.out.printf("%s", new StockName("HUGE", "Huge Fruit, Inc.", "Fruit Titanesque, Inc."));

prints Huge Fruit, Inc. as expected.

However, the following does not work:

System.out.printf("%s", new StockName("PERC", "%Company, Inc.", "Fruit Titanesque, Inc."));

It throws a java.util.MissingFormatArgumentException:

Exception in thread "main" java.util.MissingFormatArgumentException: Format specifier '%C'
        at java.util.Formatter.format(Formatter.java:2519)
        at java.util.Formatter.format(Formatter.java:2455)
        at StockName.formatTo(FormattableTest.java:44)
        at java.util.Formatter$FormatSpecifier.printString(Formatter.java:2879)
        at java.util.Formatter$FormatSpecifier.print(Formatter.java:2763)
        at java.util.Formatter.format(Formatter.java:2520)
        at java.io.PrintStream.format(PrintStream.java:970)
        at java.io.PrintStream.printf(PrintStream.java:871)
        at FormattableTest.main(FormattableTest.java:55)

The sample uses Formatter.format to add the text, while format is supposed to format a format string. This causes things to break when the text that's supposed to be appended contains a percent.

How should I deal with this in formatTo? Should I manually write to the formatter's Appendable (formatter.out().append(text), which can throw an IOException somehow)? Should I attempt to escape the format string (something like formatter.format(text.replace("%","%%")), though that may not be enough)? Should I pass a simple format string to the formatter (formatter.format("%s", text), but that seems redundant)? All of these should work, but what is the correct way semantically?

To clarify, in this hypothetical situation, the parameters given to StockName are user-controlled and can be arbitrary; I don't have exact control over them (and I can't disallow input of %). However, I am able to edit StockName.formatTo.

Pokechu22
  • 4,984
  • 9
  • 37
  • 62
  • Where exactly does it throw the exception? – Andy Turner Mar 13 '17 at 00:37
  • @AndyTurner I didn't initially include it because it mostly doesn't make sense for my contrived code, but the exception occurs within `StockName.formatTo`'s call to `format` – Pokechu22 Mar 13 '17 at 05:34
  • The right way would probably have been to call `format.out().append(text)`, however since `formatTo` does not allow throwing `IOException`s you cannot handle the exception properly. Supressing it is not good, but wrapping it is not good either, because `Formatter` only supresses `IOException`s. See also https://bugs.openjdk.java.net/browse/JDK-8079892 – Marcono1234 Apr 27 '19 at 15:58
  • 1
    There is now [JDK-8223149](https://bugs.openjdk.java.net/browse/JDK-8223149) which describes the incorrect doc example – Marcono1234 May 01 '19 at 16:07

2 Answers2

5

It is actually simple. You escape the percent characters only during formatting, not in the original property:

  // apply precision
  if (precision == -1 || out.length() < precision) {
    // write it all
    sb.append(out.replaceAll("%", "%%"));
  }
  else {
    sb.append(out.substring(0, precision - 1).replaceAll("%", "%%")).append('*');
  }

Then if you do this:

StockName stockName = new StockName("HUGE", "%Huge Fruit, Inc.", "Fruit Titanesque, Inc.");
System.out.printf("%s%n", stockName);
System.out.printf("%s%n", stockName.toString());
System.out.printf("%#s%n", stockName);
System.out.printf("%-10.8s%n", stockName);
System.out.printf("%.12s%n", stockName);
System.out.printf(Locale.FRANCE, "%25s%n", stockName);

The output looks like this:

%Huge Fruit, Inc.
HUGE - %Huge Fruit, Inc.
HUGE
HUGE      
%Huge Fruit*
   Fruit Titanesque, Inc.
kriegaex
  • 63,017
  • 15
  • 111
  • 202
  • 1
    No feedback for such a long time? Why put a bounty on a question and then ignore answers? I put some time and effort into my answer in order to help you. – kriegaex Mar 21 '17 at 16:25
  • I just didn't have any feedback on it; I'm waiting for the bounty to run through the entire way (in case more answers are attracted; it also helps reward answers because people actually _see_ them). The answer itself is good and I'll reward it after the bounty's done. (EDIT: also, looks like I didn't +1 it initially; I _thought_ I did and I did mean to, sorry!) – Pokechu22 Mar 21 '17 at 22:59
2

If you want print percentage symbol %, you must escape it by double writing, e.g.

System.out.printf("%s", new StockName("PERC", "%%Company, Inc.", "Fruit Titanesque, Inc."));

this will print %Company, Inc.

Yu Jiaao
  • 4,444
  • 5
  • 44
  • 57
  • For more info you refer to this link [java.util.Formatter](http://docs.oracle.com/javase/7/docs/api/java/util/Formatter.html#detail) – Yu Jiaao Mar 13 '17 at 01:10
  • While that's entirely reasonable, I can't do that for arbitrary user input; is there a way to automatically escape it? – Pokechu22 Mar 13 '17 at 05:34
  • You MUST escape all user input as a rule. See [this link](http://stackoverflow.com/a/42447109/1484621) and [this link](http://stackoverflow.com/questions/267487/in-delphi-7-how-do-i-escape-a-percent-sign-in-the-format-function) and [this](http://stackoverflow.com/questions/5011932/how-to-escape-in-string-format), I think you can escape the special character just by `str = str.replaceAll("%", "%%");`. BTW, the format rule has a long way inherited from the C `sprintf` function – Yu Jiaao Mar 13 '17 at 13:21
  • I understand that it needs to be escaped when calling `Formatter.format`; but the sample doesn't do that. Escaping the parameter before it's passed to the constructor seems like a bad idea (it'll break, for instance, toString, plus any theoretical application logic that tries to match on the company name given that in some situations it'll be escaped and others won't). It's possible to put the escaping within my `formatTo` method, but it still seems like bad API design to require escaping text (that isn't supposed to be a format string) just to unescape it - (cont) – Pokechu22 Mar 13 '17 at 14:22
  • (cont) If I'm writing content to the `Appendable` given to the formatter, why should it be necessary that I escape the text and then let it parse and process the entire text as a format string (which would be slower) for something that doesn't need formatting? Isn't there a cleaner way? – Pokechu22 Mar 13 '17 at 14:23
  • You are right, I think the API designer made this is for old-C-compatibility, since none attend to change this. The more java way is use `java.text.MessageFormat`, but the escape problem is still there. From this point of view, the Groovy double and single quote paradigm look more attractive. We live in such a world there no exists a perfect solution ;) – Yu Jiaao Mar 14 '17 at 00:53
  • I'm going to put a bounty onto the question just to see if there _is_ a better way (who knows, there might be another way the API was supposed to be used). If there isn't a better way, I'll accept this. – Pokechu22 Mar 15 '17 at 00:25