views:

55

answers:

4

I've found a problem with the strtod method which I've used all this time. First of all it doesn't understand non-point decimal separator, so I've forced to use this:

std::replace(sSource.begin(), sSource.end(), getDecimalSeparator(), '.');

But no I've found another problem and didn't found how to resolve it yet. If the value is negative and the thousands separator is a point ("."), strtod return 0 and _EndPtr points to the start of the string:

// PRECONDITIONS:
//  * digit grouping symbol (thousands separator) = "."
//  * decimal symbol                              = ","
//  * digital grouping                            = "123.456.789"
//  * negative sign symbol                        = "-"
//  * negative number format                      = "- 1,1"
//  * OS WinXP SP2
//  * the rest doesn't matter

double parse(const char* cszValue, char** szStop)
{
    // NOTE: error handling code has been removed to simplify the sample
    return strtod(sSource.c_str(), szStop);
}

//...
char* szStop = NULL;
double dVal = 0.0;
dVal = parse("123.45",     &szStop); // works!

// All the next DON'T WORK!
dVal = parse("123,45",     &szStop); // dVal == 123.0 szStop == ",45"
dVal = parse("- 123.45",   &szStop); // dVal == 0.0   szStop == "- 123.45"
// the same for "- 123,45"
dVal = parse("1.123.45",   &szStop); // dVal == 1.123 szStop == ".45"
// the same for "1.123,45"
dVal = parse("1 123.45",   &szStop); // dVal == 1     szStop == " 123.45"
// the same for "1 123,45"
dVal = parse("- 1 123,45", &szStop); // dVal == 0     szStop == start of the string
// the same for "- 1 123.45", "- 1.123,45", "- 1.123.45"

There are the questions:

  • What I doing wrong? (answered)

  • Why strtod fails if the decimal separator formatted according to local settings? (answered)

  • Even if I'll replace current decimal separator with a point (".") and remove all thousand separators, how to parse a negative value? Detect a negative sing, remove it, parse the value as a positive number and reverse the negative sign after that?

A: 

Locale sensitivity is not a feature of strtod(), sadly. The documentation suggests it always uses U.S. conventions.

The obvious solution is to complete the remapping of expected locale characters, in your case eliminating all spaces and . (digit grouping) and then replacing , with . (decimal) to get the functionality desired. There's no need to do anything with - and + (as far as I know, those have the same meaning in all the romantic languages).

wallyk
Oh, don't we all love those _romantic_ languages! `:)`
sbi
The conversion has no need to see any digit grouping characters, so removing them all makes sense. (I have never encountered `'` in Switzerland.)
wallyk
Indeed, removing *digit-grouping characters* is needed here, but removing spaces and `.` fails in the general case :-). Sorry though, as my criticism was unfounded as I missed the exact preconditions given in the question, so I withdraw that comment.
Joey
+1  A: 

Hi Sibvic,

Regarding incorrect results I can see the problem due to white space(s) in the digits. As per documentation strtod() this function will remove leading white spaces and stop reading when it finds the space in the mid of the digits and return the value converted to double. For example,

In case given below strtod finds the space after minus sign and minus only is not a valid numeric value so it returns 0.0.

dVal = parse("- 123.45",   &szStop); // dVal == 0.0   szStop == "- 123.45"

Similarly in case of line below, when it finds second decimal, it assumes it end of the value because two decimals are not possible in numeric value.

dVal = parse("1.123.45",   &szStop); // dVal == 1.123 szStop == ".45"

In case of line give below it finds the space after 1 and stops further processing and return the 1 as parsed double.

dVal = parse("1 123.45",   &szStop); // dVal == 1     szStop == " 123.45"

The case below resembles the first case that I have mentioned above.

dVal = parse("- 1 123,45", &szStop); // dVal == 0     szStop == start of the string

I hope this helps.

Regards, Azher Iqbal

Azher Iqbal
Now, when you've mentioned this I've got it. I've did a little experiment, there is the result: strtod (it least MS implementation) removes all digit grouping symbol from the string. If the negative sign with the empty space symbol "- " and the digit grouping symbol is " " (or "-." and ".") it works well and fails otherwise. Thanks a lot. This is why it had worked in some cases with "- " which confused me the most.
sibvic
A: 

Not sure following consideration applicable across standard but MSDN states that strtod as long as atof react to the settings set up in a call to setlocale (see LC_NUMERIC).

For the case

dVal = parse("- 123.45",   &szStop); // dVal == 0.0   szStop == "- 123.45"

...it's true that no symbols except digits may follow minus (or plus) optional sign.

Keynslug
A: 

It seems the locale-sensitive parsing should looks like this:

bool bNegative = false;
switch (getNegativeOrder())
{
// enumeration from MSDN
case 0: // Left parenthesis, number, right parenthesis; for example, (1.1)
    if (sSource[0] == '(' && sSource[sSource.size() - 1] == ')')
    {
        bNegative = true;
        sSource = sSource.substr(1, sSource.size() - 2);
    }
    break;
case 1: // Negative sign, number; for example, -1.1
    if (sSource[0] == '-')
    {
        bNegative = true;
        sSource = sSource.substr(1, sSource.size() - 1);
    }
    break;
case 2: // Negative sign, space, number; for example, - 1.1
    if (sSource.size() > 1 && sSource[0] == '-' && sSource[1] == ' ')
    {
        bNegative = true;
        sSource = sSource.substr(2, sSource.size() - 2);
    }
    break;
case 3: // Number, negative sign; for example, 1.1-
    if (sSource[sSource.size() - 1] == '-')
    {
        bNegative = true;
        sSource = sSource.substr(0, sSource.size() - 1);
    }
    break;
case 4: // Number, space, negative sign; for example, 1.1 -
    if (sSource.size() > 1 && sSource[sSource.size() - 1] == '-' && sSource[sSource.size() - 2] == ' ')
    {
        bNegative = true;
        sSource = sSource.substr(0, sSource.size() - 2);
    }
    break;
}

// Remove thousand separator, because strtod will fail to parse them.
sSource.erase(std::remove(sSource.begin(), sSource.end(), getThousandSeparator()), sSource.end());

// strtod expects nptr to point to a string of the following form:
// [whitespace] [sign] [digits] [.digits] [ {d | D | e | E}[sign]digits]
std::replace(sSource.begin(), sSource.end(), getDecimalSeparator(), '.');

char *szStop = NULL;
double dValue = strtod(sSource.c_str(), &szStop);
if (bNegative)
    dValue *= -1;
sibvic