Decimal point inconsistency in double/string conversions

We stumbled upon an issue with the way double attributes are converted in the XmlElement class.

When executed within a plugin on a system with German locale (!), the XmlElement::setAttribute(String, double) method will use a comma as delimiter. When parsing the attribute with XmlElement::getDoubleAttribute() everything behind the comma is discarded.

We narrowed it down further:
NumberToStringConverters::doubleToString will use sprintf if the number of decimal places isn’t specified. In a ProTools plugin, this will result in a comma.
In CharacterFunctions::readDoubleValue the decimal point is hard-coded as “.”

We didn’t see this behaviour in the standalone version or any other host than ProTools. Nevertheless, it feels wrong that the XmlElement’s setAttribute() methods use conversions that can depend on the system’s locale.

Here’s a paint method to reproduce this issue with a basic IntroJucer plugin project:

void RtasLocalePluginAudioProcessorEditor::paint (Graphics& g)
{
    g.fillAll (Colours::white);
    g.setColour (Colours::black);
    g.setFont (15.0f);

    String attrName = "attr";
    double attrValue = 12345.678;

    XmlElement xml ("xml");
    xml.setAttribute(attrName, attrValue);

    String msg;
    msg << "stringified with String(double, int) " << String(attrValue, 4) << "\n";
    msg << "stringified with ::setAttribute(String, double): " << xml.getStringAttribute(attrName) << "\n";
    msg << "parsed with getDoubleAttribute():" << xml.getDoubleAttribute(attrName) << "\n";

    g.drawFittedText (msg, 0, 0, getWidth(), getHeight(), Justification::centred, 1);
}

Hmm. That’s an annoying one to fix!

I suppose the correct thing to do would be to write my own exponential string formatter, but haven’t time to do that right now… An alternative would be to add some code that checks the result of sprintf and replaces any commas with dots, but that’d be an awful hack! (But you might want to add a quick bit of code yourself to do that while I have a think about it)

Hm, I think this should do the trick (at least it seems to work in PT)

in juce_String.cpp at the beginning:

#include <locale>
#if !JUCE_MSVC
#include <xlocale.h>
#endif

and in doubleToString (char* buffer, int numChars, double n, int numDecPlaces, size_t& len)

...
        else
        {   
#if JUCE_MSVC
            len = _sprintf_l (buffer, "%.9g", nullptr, n);
#else
            len = sprintf_l (buffer, nullptr, "%.9g", n);
#endif
            return buffer;
        }
...

this forces sprintf to use a dot.

cheers
tim

Ah! Great tip! Thanks, I had no idea sprintf_l existed!

yeah, me neither… until now.

however, I think that the double-writing and reading in the String class is still a bit unconsistent. In doubleToString (…) if numDecPlaces is > 0, you do the following:

while (numDecPlaces >= 0 || v > 0)
            {
                if (numDecPlaces == 0)
                    *--t = (char) getDecimalPoint();

                *--t = (char) ('0' + (v % 10));

                v /= 10;
                --numDecPlaces;
            }

where getDecimalPoint uses the locale symbol (so either dot or comma), but in juce_CharacterFunctions.h in readDoubleValue (CharPointerType& text) you assume the decimal point to be a dot.
Maybe it would be better to omit the getDecimalPoint and stick with the dot?!?! Then things would be at least consistent.
What do you think?

cheers
tim

Yes… You might be right there. Not 100% sure why I made it use the locale for the separator char there. Thanks, I’ll think about that…

We had to change the _sprintf_l solution to be effective on my windows system. Just passing null didn’t work as intended, so we explicitly request the C locale now:

len = _sprintf_l (buffer, "%.9g", _create_locale(LC_NUMERIC, "C"), n);

Ok, thanks for that.