Rounding the SIGNIFICANT digits in a double, not t

2019-09-18 03:36发布

问题:

This question already has an answer here:

  • Round a double to x significant figures 12 answers

I need to round significant digits of doubles. Example Round(1.2E-20, 0) should become 1.0E-20

I cannot use Math.Round(1.2E-20, 0), which returns 0, because Math.Round() doesn't round significant digits in a float, but to decimal digits, i.e. doubles where E is 0.

Of course, I could do something like this:

double d = 1.29E-20;
d *= 1E+20;
d = Math.Round(d, 1);
d /= 1E+20;

Which actually works. But this doesn't:

d = 1.29E-10;
d *= 1E+10;
d = Math.Round(d, 1);
d /= 1E+10;

In this case, d is 0.00000000013000000000000002. The problem is that double stores internally fractions of 2, which cannot match exactly fractions of 10. In the first case, it seems C# is dealing just with the exponent for the * and /, but in the second case it makes an actual * or / operation, which then leads to problems.

Of course I need a formula which always gives the proper result, not only sometimes.

Meaning I should not use any double operation after the rounding, because double arithmetic cannot deal exactly with decimal fractions.

Another problem with the calculation above is that there is no double function returning the exponent of a double. Of course one could use the Math library to calculate it, but it might be difficult to guarantee that this has always precisely the same result as the double internal code.

In my desperation, I considered to convert a double to a string, find the significant digits, do the rounding and convert the rounded number back into a string and then finally convert that one to a double. Ugly, right ? Might also not work properly in all case :-(

Is there any library or any suggestion how to round the significant digits of a double properly ?

PS: Before declaring that this is a duplicate question, please make sure that you understand the difference between SIGNIFICANT digits and decimal places

回答1:

The problem is that double stores internally fractions of 2, which cannot match exactly fractions of 10

That is a problem, yes. If it matters in your scenario, you need to use a numeric type that stores numbers as decimal, not binary. In .NET, that numeric type is decimal.

Note that for many computational tasks (but not currency, for example), the double type is fine. The fact that you don't get exactly the value you are looking for is no more of a problem than any of the other rounding error that exists when using double.

Note also that if the only purpose is for displaying the number, you don't even need to do the rounding yourself. You can use a custom numeric format to accomplish the same. For example:

double value = 1.29e-10d;
Console.WriteLine(value.ToString("0.0E+0"));

That will display the string 1.3E-10;

Another problem with the calculation above is that there is no double function returning the exponent of a double

I'm not sure what you mean here. The Math.Log10() method does exactly that. Of course, it returns the exact exponent of a given number, base 10. For your needs, you'd actually prefer Math.Floor(Math.Log10(value)), which gives you the exponent value that would be displayed in scientific notation.

it might be difficult to guarantee that this has always precisely the same result as the double internal code

Since the internal storage of a double uses an IEEE binary format, where the exponent and mantissa are both stored as binary numbers, the displayed exponent base 10 is never "precisely the same as the double internal code" anyway. Granted, the exponent, being an integer, can be expressed exactly. But it's not like a decimal value is being stored in the first place.

In any case, Math.Log10() will always return a useful value.

Is there any library or any suggestion how to round the significant digits of a double properly ?

If you only need to round for the purpose of display, don't do any math at all. Just use a custom numeric format string (as I described above) to format the value the way you want.

If you actually need to do the rounding yourself, then I think the following method should work given your description:

static double RoundSignificant(double value, int digits)
{
    int log10 = (int)Math.Floor(Math.Log10(value));
    double exp = Math.Pow(10, log10);
    value /= exp;
    value = Math.Round(value, digits);
    value *= exp;

    return value;
}