DEV Community

Petter Holmström
Petter Holmström

Posted on

A Potentially Expensive Mistake

Once upon a time, there was a project. We upgraded its JVM from Java 11 to Java 17 and did some smoke testing. Everything looked fine, except our financial values, which were not formatted correctly anymore.

We had been using the standard NumberFormat class with the correct locale in order to ensure the correct formatting. Java 11 did this right and formatted numbers with spaces as thousand separators, like this: 1 000 000. Java 17, for some strange reason, decided to change this and used commas instead, like this: 1,000,000, even though the locale was exactly the same.

We of course needed to fix this, so we changed the DecimalFormatSymbols like this:

var formatter = NumberFormat.getNumberInstance(locale);
var symbols = ((DecimalFormat) formatter)
symbols.setGroupingSeparator(' ');
symbols.setMonetaryGroupingSeparator(' ');
((DecimalFormat) formatter).setDecimalFormatSymbols(symbols);
Enter fullscreen mode Exit fullscreen mode

This did the trick and the financial values were again formatted correctly and all was well, right?

Unfortunately not. You can also use NumberFormat to parse strings and in a few places of the code, we did just this. When we were using only NumberFormat.getNumberInstance(locale) without any customization, this worked exactly as it was supposed to. Once we started to do customization of the formatting, things took a dangerous turn that was not picked up by any of our unit tests.

If you take a value formatted by the customized formatter and asks a non-customized formatter to parse it, like this, what do you get?

var formatted = formatter.format(1000000); // "1 000 000"
var parsed = NumberFormat.getNumberInstance(locale)
    .parse(formatted); // ??
Enter fullscreen mode Exit fullscreen mode

It turns out, the parsed variable above will not contain the value of 1000000, but 1!

You see, the NumberFormat.parse method only parses the beginning of the string. Once it encounters a character it does not recognize, it stops and returns the result. For example this code:

formatter.parse("1000 pirates on a ship");
Enter fullscreen mode Exit fullscreen mode

will not throw an exception but return 1000 and discard the rest of the string. I was not aware of this, even though it says so in the JavaDocs. This also happened with the parsing of the financial values, since space as a thousand separator was not a valid character from the point of view of the uncustomized formatter.

Needless to say, converting one million into one is quite a serious bug. In our case, it made it all the way into production before we caught it but luckily, it had not managed to cause any damage yet.

I had not expected a change of VM to have this kind of effect, but it is often the unexpected bugs that end up biting you the hardest.

Top comments (0)