In this GIF, you can see what is wrong:
The code for this JFormattedTextField and the JFormatter looks like this:
NumberFormat format = NumberFormat.getCurrencyInstance(Locale.GERMANY);
format.setMaximumFractionDigits(2);
NumberFormatter formatter = new NumberFormatter(format);
formatter.setAllowsInvalid(false);
formatter.setOverwriteMode(true);
JFormattedTextField price = new JFormattedTextField(formatter);
price.setValue(0.00);
Every time a number hits the separator, it adds two additional zeros. How can I prevent that?
You are overwriting the decimal separator. When the contained text is 1,00 €
, corresponding to 1.0
, and you overwrite the ,
with 1
, the resulting text first is 1100 €
which will be parsed to 1100.0
as the NumberFormat
is tolerant regarding absent or misplaced grouping separators, as well as absent fractional parts. The number 1100.0
is then reformatted to 1.100,00 €
.
This repeats when you continue typing.
The fact that the comma gets overwritten seems to be a bug. I found the following code in the NumberFormatter
:
/**
* Subclassed to treat the decimal separator, grouping separator,
* exponent symbol, percent, permille, currency and sign as literals.
*/
boolean isLiteral(Map<?, ?> attrs) {
if (!super.isLiteral(attrs)) {
if (attrs == null) {
return false;
}
int size = attrs.size();
if (attrs.get(NumberFormat.Field.GROUPING_SEPARATOR) != null) {
size--;
if (attrs.get(NumberFormat.Field.INTEGER) != null) {
size--;
}
}
if (attrs.get(NumberFormat.Field.EXPONENT_SYMBOL) != null) {
size--;
}
if (attrs.get(NumberFormat.Field.PERCENT) != null) {
size--;
}
if (attrs.get(NumberFormat.Field.PERMILLE) != null) {
size--;
}
if (attrs.get(NumberFormat.Field.CURRENCY) != null) {
size--;
}
if (attrs.get(NumberFormat.Field.SIGN) != null) {
size--;
}
return size == 0;
}
return true;
}
Contrary to the documentation comment’s enumeration, which mentions “decimal separator”, the field NumberFormat.Field.DECIMAL_SEPARATOR
is not handled within the method.
Since this is a package-private method, as most of the implementation, there is no way to alter this behavior from an application. We can place a work-around at the incoming NumberFormat
side, by creating a custom NumberFormat
which removes the marker for the DECIMAL_SEPARATOR
at the character iterator already.
First, we add a helper method
/**
* Converts DECIMAL_SEPARATOR fields to literal text.
*/
static AttributedCharacterIterator
convertSeparatorFieldToLiteral(AttributedCharacterIterator aci) {
if(aci.current() == CharacterIterator.DONE) return aci;
int o = aci.getBeginIndex();
StringBuilder sb = new StringBuilder(aci.getEndIndex()-o);
do sb.append(aci.current()); while(aci.next() != CharacterIterator.DONE);
AttributedString s = new AttributedString(sb.toString());
for(aci.first(); aci.current() != AttributedCharacterIterator.DONE;
aci.setIndex(aci.getRunLimit())) {
Map<AttributedCharacterIterator.Attribute, Object> attr = aci.getAttributes();
if(attr == null || attr.isEmpty()) continue;
if(attr.containsKey(NumberFormat.Field.DECIMAL_SEPARATOR)) {
if(attr.size() == 1) continue;
attr = new HashMap<>(attr);
attr.remove(NumberFormat.Field.DECIMAL_SEPARATOR);
}
s.addAttributes(attr, aci.getRunStart()-o, aci.getRunLimit()-o);
}
return s.getIterator();
}
Then, change the example usage to
NumberFormat format = NumberFormat.getCurrencyInstance(Locale.GERMANY);
format.setMaximumFractionDigits(2);
NumberFormat myFormat = new NumberFormat() {
@Override
public StringBuffer format(double number, StringBuffer toAppendTo, FieldPosition p) {
return format.format(number, toAppendTo, p);
}
@Override
public StringBuffer format(long number, StringBuffer toAppendTo, FieldPosition pos) {
return format.format(number, toAppendTo, pos);
}
@Override
public Number parse(String source, ParsePosition parsePosition) {
return format.parse(source, parsePosition);
}
@Override
public AttributedCharacterIterator formatToCharacterIterator(Object obj) {
AttributedCharacterIterator aci = format.formatToCharacterIterator(obj);
return convertSeparatorFieldToLiteral(aci);
}
};
NumberFormatter formatter = new NumberFormatter(myFormat);
formatter.setAllowsInvalid(false);
formatter.setOverwriteMode(true);
JFormattedTextField price = new JFormattedTextField(formatter);
price.setValue(0.00);
The custom NumberFormat
delegates to the original format in almost every regard. The only difference is that the DECIMAL_SEPARATOR
attribute gets removed from the character iterator, so the decimal separator will be treated like literal text of the format which can not be changed.
As a result, you can type straight-forwardly in overwrite mode. However, the handling of a JFormattedTextField
in overwrite mode still is tricky. You need to setMinimumIntegerDigits(…)
to make writing numbers with more digits easier, but deleting decimal digits will become unintuitive then.