Wednesday, June 21, 2006

Answer: Using JFormattedTextField for integers

I wrote earlier about using JFormattedTextField for integers and the difference between using an initial value of Integer (rounds input) v. BigInteger (rejects invalid input). I also wrote that I did not understand why there was a difference.

I now know what happened.

The culprit for handling Integer turns out to be NumberFormat and friends. The rounding behavior is not numerical rounding at all; it is truncating the input text.

The parser for the input stops after finding valid input and returns a parsed value in stringToValue(String):Object. This discards any trailing text, which in the case of parsing an integer is everything from the decimal point forward. The same behavior can be seen with a date parser: Jun 21, 2006xxx parses to Jun 21, 2006 and the xxx is discarded.

The solution to enforce correct input even with trailing garbage is cumbersome: initialize JFormattedTextField with a custom formatter and check that parsed input deparses back cleanly:

new JFormattedTextField(new Integer(0))

becomes:

new JFormattedTextField(new NumberFormatter(
        NumberFormat.getIntegerInstance()) {
    @Override
    public Object stringToValue(final String text)
            throws ParseException {
        // Throws if the input is corrupt from the start
        final Object parsed = super.stringToValue(text);
        final String deparsed = valueToString(parsed);

        // Throws if there is no clean roundtrip,
        // such as trailing garbage characters
        // For date parsing, etc., consider equalsIgnoreCase
        if (!deparsed.equals(text))
            throw new ParseException(text, deparsed.length());

        return parsed;
    }
}) {
    {
        // Initialize to the original starting value
        setValue(new Integer(0));
    }
}

(Yes, the anonymous instance syntax is awkward here.)

That's a lot of work to achieve what seems like a simple goal: actually valid input for a JFormattedTextField.

UPDATE: Given the hoops to jump through for this more "correct" solution, I think I'll stick to new JFormattedTextField(new BigInteger(0)) for now, although that does nothing to help with other input types such as dates.

1 comment:

Ondrej Medek said...

I have created a subclass of the DecimalFormat, where I override:

/* (non-Javadoc)
* @see java.text.Format#parseObject(java.lang.String)
*/
@Override
public Object parseObject(String source) throws ParseException {
ParsePosition pos = new ParsePosition(0);
Object result = parseObject(source, pos);

int index = pos.getIndex();
if (index == 0) { // nothing has been parsed
throw new ParseException("Unparseable number: \"" + source + "\"",
pos.getErrorIndex());
}

int length = source.length();
for (; index < length; index++) {
if (!Character.isWhitespace(source.charAt(index))) {
// non-white space before end - error
throw new ParseException("Unparseable number: \"" + source + "\"",
index);
}
}

return result;
}

/* (non-Javadoc)
* @see java.text.NumberFormat#parse(java.lang.String)
*/
@Override
public Number parse(String source) throws ParseException {
ParsePosition pos = new ParsePosition(0);
Number result = parse(source, pos);

int index = pos.getIndex();
if (index == 0) { // nothing has been parsed
throw new ParseException("Unparseable number: \"" + source + "\"",
pos.getErrorIndex());
}

int length = source.length();
for (; index < length; index++) {
if (!Character.isWhitespace(source.charAt(index))) {
// non-white space before end - error
throw new ParseException("Unparseable number: \"" + source + "\"",
index);
}
}

return result;
}




and then use it as

new JFormattedTextField(new MyDecimalFormat());