0

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

BartoszKP
  • 34,786
  • 15
  • 102
  • 130
Peter Huber
  • 3,052
  • 2
  • 30
  • 42
  • by "fractions of 2" do you mean "factors of 2"? – Sam Axe Mar 20 '15 at 06:20
  • 3
    instead of using a `double` how about using a `Decimal`. – Sam Axe Mar 20 '15 at 06:21
  • Decimal is significantly slower but it is more accurate. Also depends on the range of values you need. – kjbartel Mar 20 '15 at 11:51
  • The guys marking this question as duplicate, can they please provide a link to the question they think is the same. I went through hundreds of rounding questions on stackoverflow. I couldn't find one asking my problem and would be more than happy to get a link. – Peter Huber Mar 21 '15 at 00:50
  • I cannot use decimals, since I write a chart using doubles. It covers the whole double range. It supports also zooming and scrolling, which leads to all kind of visible problems with the labels and grid lines if the rounding goes wrong. – Peter Huber Mar 21 '15 at 00:52
  • @DrKoch: Please provide a link to the question you feel is the same as here. – Peter Huber Mar 21 '15 at 00:58
  • @kenorb: Please provide a link to the question you feel is the same as here. – Peter Huber Mar 21 '15 at 00:58
  • @ElmoVanKielmo: Please provide a link to the question you feel is the same as here. – Peter Huber Mar 21 '15 at 00:59
  • @winwaed: Please provide a link to the question you feel is the same as here. – Peter Huber Mar 21 '15 at 00:59
  • @chridam: Please provide a link to the question you feel is the same as here. Sorry for the many comments. I went to each persons user page, but couldn't figure out how to send them a message. Comments allow only to address 1 person at a time, so I had to make 5 comments :-( – Peter Huber Mar 21 '15 at 01:00
  • The question "Round a double to x significant figures" has no proper solution for my problem. As I showed in my question, rounding and then using any double operation doesn't work, as it is stated in those answers too. There is another answer there suggesting to use decimal. That poster highlights himself that his solution doesn't work for the whole double value range ... – Peter Huber Mar 21 '15 at 01:17
  • The only way in which the marked duplicate isn't a duplicate of your question is that you have more than one question in your question and it doesn't necessarily address all of them. But it certainly addresses in as best a way possible your request to round to N significant digits. As far as your charting needs, if you have visible artifacts due to rounding, there's probably something else wrong with your code. You should ask a specific question about _that_. Finally, `decimal` has more precision than `double`, so your concerns about using it instead don't make sense. – Peter Duniho Mar 21 '15 at 02:04
  • @PeterDuniho: The value range of decimal is 7.9 * (28 to the power of 10). The value range of double is 1.7 * (308 to the power of 10). Obviously it is not possible to cover the value range of doubles with decimals. If you don't believe it, try to assign double.MaxValue and you will get the exception: "Value was either too large or too small for a Decimal." Makes sense ? – Peter Huber Mar 21 '15 at 02:46
  • Yes, but can you not scale your input values to fit within the `decimal` range? As I mentioned, `decimal` has more precision, so you should not lose anything significant in conversion. That said, as I also mentioned, there is no reason that you should run into visual artifacts due to limits of the `double` type, and in any case doing _more_ rounding certainly would not fix that. If you have problems getting `double` to work in a charting scenario, you would do better to ask questions focused on that, rather than asking for the impossible. – Peter Duniho Mar 21 '15 at 02:56
  • @PeterDuniho: I have only 1 question: How to get significant digits rounding of double done properly. Then I showed in my question that the approach converting the double into another double with exponent 0, using Math.Round() for rounding and then converting back doesn't work, providing a numerical example. – Peter Huber Mar 21 '15 at 03:02
  • The problem is simple. To add grid lines, I have to calculate their values, like GridValue(n) = startValue + (n*stepvalue), where n is the nth gridline. Example: GridValue(n) = 1.23E+128 + (n*1.0E+127). I then need to compare if the gridline is within the valuerange, say 1.23E+128 ... 1.27E+128. The problem is that 1.23E+128 + (40*1.0E+127) might not be equal to 1.27E+128 in double arithmetic, depending on the numbers. So I need to round the result before comparing the sum with the end value to decide if the last gridline should be shown. Of course, zooming and paging face the same problem. – Peter Huber Mar 21 '15 at 03:11
  • Translating doubles somehow to decimal and then translating back might face the same problems like multiplying/dividing doubles to use Math.Round(), because the limited decimal range needs to be extended using double arithmetic, which has the problems I demonstrated. It is definitely not trivial. It would be interesting to see some working code. Thanks for your thoughts, though. – Peter Huber Mar 21 '15 at 03:18
  • @dan I am not sure if the expression "fractions of 2" is correct, but what I mean is: 1/2, 1/(2*2), 1/(2*2*2), 1/(2*2*2*2) – Peter Huber Mar 21 '15 at 03:26
  • "How to get significant digits rounding of double done properly" -- your only hope is to define "properly" in a way that does not ask for the impossible. So far, as has been explained multiple times in multiple ways by multiple people, you are asking for the impossible. The `double` type simply is not capable of producing the result you insist on. You might as well as your PC to fly itself to the moon. – Peter Duniho Mar 21 '15 at 05:39
  • If Microsoft could do decimal point rounding properly for doubles, I think it is dangerous to state categorically that proper significant digits rounding is impossible. By proper, I mean that Round(a.bcE-de, 1) - a.bE-de ==0. a,b,c,d,e are digits, for example Round(1.29E-10, 1) - 1.2E-10 ==0. I can see at least one way to achieve that: convert 1.29E-10 to a string, remove '1', convert back, which should obviously result in 1.29E-10. If you still feel this is impossible, please explain why. And a bit less agressively would be appreciated too. – Peter Huber Mar 21 '15 at 10:01

1 Answers1

5

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;
}
Peter Duniho
  • 68,759
  • 7
  • 102
  • 136
  • That's almost the same code I use myself. But for a double using `Math.Pow` and multiplying or dividing by the exp will often lead to junk in the lower digits so you'll still need to format the number to the number of required significant digits. – kjbartel Mar 20 '15 at 11:44
  • @kjbartel: "so you'll still need to format the number to the number of required significant digits" -- yes, exactly. That's why I recommend using `decimal` if that "junk" is actually a problem (e.g. the output isn't being formatted for some reason). It's just not possible to accomplish an exact rounding in all cases using `double` (just as it's not possible to always have an exact decimal value in any other scenario using `double`). – Peter Duniho Mar 20 '15 at 11:52
  • Thanks for the long and detailed answer. Unfortunately, your final suggestion is the same as the one I posted, with the same problem, i.e. that executing any arithmetic operations with doubles after the rounding will spoil the rounding. – Peter Huber Mar 21 '15 at 01:04
  • Actually my code example is not the same as yours, as it actually works for arbitrary inputs (i.e. computes the exponent). In any case, the "problem" you describe is impossible to "solve" if you insist on using the `double` type. **It is simply impossible for the `double` type to represent the value you want it to represent.** – Peter Duniho Mar 21 '15 at 02:01
  • It is true that it is impossible for double to store 1.29E-10 exactly. But what I am looking for is a formula which returns the exactly the same number for round(1.291E-10, 1) and 1.29E-10. @PeterDuniho: I plussed 1 your answer to show my appreciation. – Peter Huber Mar 21 '15 at 02:38