Round a double to x significant figures

70,777

Solution 1

The framework doesn't have a built-in function to round (or truncate, as in your example) to a number of significant digits. One way you can do this, though, is to scale your number so that your first significant digit is right after the decimal point, round (or truncate), then scale back. The following code should do the trick:

static double RoundToSignificantDigits(this double d, int digits){
    if(d == 0)
        return 0;

    double scale = Math.Pow(10, Math.Floor(Math.Log10(Math.Abs(d))) + 1);
    return scale * Math.Round(d / scale, digits);
}

If, as in your example, you really want to truncate, then you want:

static double TruncateToSignificantDigits(this double d, int digits){
    if(d == 0)
        return 0;

    double scale = Math.Pow(10, Math.Floor(Math.Log10(Math.Abs(d))) + 1 - digits);
    return scale * Math.Truncate(d / scale);
}

Solution 2

I've been using pDaddy's sigfig function for a few months and found a bug in it. You cannot take the Log of a negative number, so if d is negative the results is NaN.

The following corrects the bug:

public static double SetSigFigs(double d, int digits)
{   
    if(d == 0)
        return 0;

    decimal scale = (decimal)Math.Pow(10, Math.Floor(Math.Log10(Math.Abs(d))) + 1);

    return (double) (scale * Math.Round((decimal)d / scale, digits));
}

Solution 3

It sounds to me like you don't want to round to x decimal places at all - you want to round to x significant digits. So in your example, you want to round 0.086 to one significant digit, not one decimal place.

Now, using a double and rounding to a number of significant digits is problematic to start with, due to the way doubles are stored. For instance, you could round 0.12 to something close to 0.1, but 0.1 isn't exactly representable as a double. Are you sure you shouldn't actually be using a decimal? Alternatively, is this actually for display purposes? If it's for display purposes, I suspect you should actually convert the double directly to a string with the relevant number of significant digits.

If you can answer those points, I can try to come up with some appropriate code. Awful as it sounds, converting to a number of significant digits as a string by converting the number to a "full" string and then finding the first significant digit (and then taking appropriate rounding action after that) may well be the best way to go.

Solution 4

If it is for display purposes (as you state in the comment to Jon Skeet's answer), you should use Gn format specifier. Where n is the number of significant digits - exactly what you are after.

Here is the the example of usage if you want 3 significant digits (printed output is in the comment of each line):

    Console.WriteLine(1.2345e-10.ToString("G3"));//1.23E-10
    Console.WriteLine(1.2345e-5.ToString("G3")); //1.23E-05
    Console.WriteLine(1.2345e-4.ToString("G3")); //0.000123
    Console.WriteLine(1.2345e-3.ToString("G3")); //0.00123
    Console.WriteLine(1.2345e-2.ToString("G3")); //0.0123
    Console.WriteLine(1.2345e-1.ToString("G3")); //0.123
    Console.WriteLine(1.2345e2.ToString("G3"));  //123
    Console.WriteLine(1.2345e3.ToString("G3"));  //1.23E+03
    Console.WriteLine(1.2345e4.ToString("G3"));  //1.23E+04
    Console.WriteLine(1.2345e5.ToString("G3"));  //1.23E+05
    Console.WriteLine(1.2345e10.ToString("G3")); //1.23E+10

Solution 5

I found two bugs in the methods of P Daddy and Eric. This solves for example the precision error that was presented by Andrew Hancox in this Q&A. There was also a problem with round directions. 1050 with two significant figures isn't 1000.0, it's 1100.0. The rounding was fixed with MidpointRounding.AwayFromZero.

static void Main(string[] args) {
  double x = RoundToSignificantDigits(1050, 2); // Old = 1000.0, New = 1100.0
  double y = RoundToSignificantDigits(5084611353.0, 4); // Old = 5084999999.999999, New = 5085000000.0
  double z = RoundToSignificantDigits(50.846, 4); // Old = 50.849999999999994, New =  50.85
}

static double RoundToSignificantDigits(double d, int digits) {
  if (d == 0.0) {
    return 0.0;
  }
  else {
    double leftSideNumbers = Math.Floor(Math.Log10(Math.Abs(d))) + 1;
    double scale = Math.Pow(10, leftSideNumbers);
    double result = scale * Math.Round(d / scale, digits, MidpointRounding.AwayFromZero);

    // Clean possible precision error.
    if ((int)leftSideNumbers >= digits) {
      return Math.Round(result, 0, MidpointRounding.AwayFromZero);
    }
    else {
      return Math.Round(result, digits - (int)leftSideNumbers, MidpointRounding.AwayFromZero);
    }
  }
}
Share:
70,777

Related videos on Youtube

Rocco
Author by

Rocco

Updated on July 05, 2022

Comments

  • Rocco
    Rocco almost 2 years

    If I have a double (234.004223), etc., I would like to round this to x significant digits in C#.

    So far I can only find ways to round to x decimal places, but this simply removes the precision if there are any 0s in the number.

    For example, 0.086 to one decimal place becomes 0.1, but I would like it to stay at 0.08.

    • Alexander Aleksandrovič Klimov
      Alexander Aleksandrovič Klimov over 15 years
      I'm not clear what you mean here. In your example, are you trying to round to 2 decimal places? Or leave just one digit? If the latter, it should be 0.09, surely, rounding up the 6...
    • Alexander Aleksandrovič Klimov
      Alexander Aleksandrovič Klimov over 15 years
      Or are you looking for N * 10^X, where N has a specified number of digits?
    • Alexander Aleksandrovič Klimov
      Alexander Aleksandrovič Klimov over 15 years
      Please give us some more examples of original numbers and what you want to see as output
    • Rocco
      Rocco over 15 years
      Rounding to significant digits is not the same as rounding to decimal places. 0.3762 to 2 decimal places is 0.38 where as to 2 significant figures/digits it is 0.37 0.0037 to 2 decimal places will correctly be 0.00 but to 2 significant digits it is 0.0037 because 0s are not significant
    • P Daddy
      P Daddy over 15 years
      I disagree. Rounding to significant digits doesn't mean that you should automatically truncate instead of round. For example, see en.wikipedia.org/wiki/Significant_figures. "... if rounding 0.039 to 1 significant figure, the result would be 0.04."
    • P Daddy
      P Daddy over 15 years
      Note, though, that I've provided both below. 0.039.RoundToSignificantDigits(1) would return 0.04, and 0.039.TruncateToSignificantDigits(1) would return 0.03.
    • Rocco
      Rocco over 15 years
      That is correct, I was mistaken in thinking they are truncated.
    • strager
      strager over 15 years
      Similar question : stackoverflow.com/questions/304011/… You can simply add round() in the appropriate place.
  • Rocco
    Rocco over 15 years
    It is for display purposes, I haven't considered a Decimal at all to be honest. How would I go about converting to string with the relevant number of significant digits as you say? I have been unable to find an example in the Double.ToString() method spec.
  • Admin
    Admin over 14 years
    Math.round(...) doesn't take two parameters
  • P Daddy
    P Daddy over 14 years
    @leftbrainlogic: Yes, it really does: msdn.microsoft.com/en-us/library/75ks3aby.aspx
  • Andrew Hancox
    Andrew Hancox over 14 years
    For some reason this code won't convert 50.846113537656557 to 6 sigfigs accurately, any ideas?
  • Fraser
    Fraser about 12 years
    Neither of these methods will work with negative numbers as Math.Log10 will return Double.NaN if d < 0.
  • P Daddy
    P Daddy about 12 years
    @Fraser: Good catch. I'll leave it as an exercise for the reader to add Math.Abs.
  • Fraser
    Fraser about 12 years
    @PDaddy hmmm, you would need to check if d == 0 as this will result in Double.NaN too - both methods need a couple of guard clauses such as: if(d == 0) { return 0; } if(d < 0) { d = Math.Abs(d); } - otherwise you end up with a division by 0 in both.
  • P Daddy
    P Daddy about 12 years
    @Fraser: Well, there goes the exercise for the reader. Btw, Eric noticed (stackoverflow.com/a/1925170/36388) the negative numbers flaw over two years ago (not the zero one, though). Maybe I should actually fix this code so people stop calling me on it.
  • farfareast
    farfareast over 11 years
    @Rocco: I know I am 4 years late, but I just came across your question. I think you should use Double.ToString("Gn"). See my answer of Nov 6 2012 :-)
  • Oliver Bock
    Oliver Bock over 11 years
    Fails for RoundToSignificantDigits(.00000000000000000846113537656557, 6) because Math.Round will not allow its second parameter to go beyond 15.
  • Evgeniy Berezovsky
    Evgeniy Berezovsky about 11 years
    @PDaddy Yes please fix it. I'd +1 it if it was fixed. I guess a lot of folks wrongly take highly voted answers as copy-and-pasteable.
  • P Daddy
    P Daddy about 11 years
    Okay, edited to handle numbers ≤ 0. Note, though, that there remains one tiny inconsistency. If you call the Round... function with a value for digits that's outside the range [0, 15], Math.Round will throw an argument exception. If d is zero, then Math.Round isn't called, so there is no exception. Insignificant, really. Note that this doesn't apply to the Truncate... function.
  • Akshay
    Akshay about 10 years
    @PDaddy your solution works like a charm, but in few cases it gives results with 01 in the end 119.212024802412, 121.198454653809. can you please tell me why 01 comes at the last instead of 00
  • P Daddy
    P Daddy about 10 years
    @aarn: I'm not sure if the numbers you gave are supposed to be inputs or outputs, and if they're inputs, you didn't list the "digits" input, so I can't say with 100% certainty what you're experiencing, but it's likely that you're seeing artifacts of translating between base-10 and base-2 floating-point representations. Please see questions like stackoverflow.com/questions/1089018 and stackoverflow.com/questions/18389455.
  • Akshay
    Akshay about 10 years
    @PDaddy sorry for insufficient info, the numbers are inputs and the digits are 15
  • P Daddy
    P Daddy about 10 years
    @aarn: When I use your first number (119.212...), the output is the exact same as the input. When I use your second number (121.198...), the output is 1.4E-14 higher. This problem is that when the number is scaled, the mantissa changed (since the exponent is base 2), and some precision is lost off the end. Perhaps scaling the other direction would eliminate this loss of precision. But keep in mind that neither your input nor your output equals 121.198454653809 exactly. For more information, see the other questions I linked to before, or perhaps open a new question.
  • Yinda Yin
    Yinda Yin almost 10 years
    That only gets you the number of significant digits to the right of the decimal point.
  • Gustav
    Gustav almost 9 years
    That returns 2340.0004 - it least with some localisations.
  • u8it
    u8it almost 8 years
    Although close, this doesn't always return sigfigs... for instance, G4 would remove the zeros from 1.000 --> 1. Also, it forces scientific notation at its discretion, whether you like it or not.
  • farfareast
    farfareast over 7 years
    Should probably agree with you on dropping significant zeros in 1.0001. As for the second statement -- the use of scientific notation is decided based on the fact which notation will take less space on print (it's an old FORTRAN rule for G format). So, in a way it is predictable, but if somebody generally prefers scientific format - it is not nice for them.
  • LearningJrDev
    LearningJrDev over 7 years
    Fails with (0.073699979, 7) returns 0.073699979999999998
  • Sнаđошƒаӽ
    Sнаđошƒаӽ over 7 years
    The question is tagged C#
  • Derrick Moeller
    Derrick Moeller over 6 years
    I would argue, 1050 rounded to two significant digits is 1000. Round to even is a very common rounding method.
  • Ramakrishna Reddy
    Ramakrishna Reddy about 4 years
    above code producing the value 5.8999999999999995E-12 for 5.9E-12 in .Net Core 3.0
  • P Daddy
    P Daddy about 4 years
    @RamakrishnaReddy: Please see questions like stackoverflow.com/questions/1089018 and stackoverflow.com/questions/18389455.
  • Amr Ali
    Amr Ali over 3 years
    Console.WriteLine("{0:G17}", RoundToSignificantDigits(5.015 * 100, 15)) gives me 501.49999999999994 which is incorrect. The right answer should be 501.5
  • P Daddy
    P Daddy over 3 years
    @AmrAli: This is because 5.015 can't be represented exactly in IEEE 754 floating point representation. Try 5.015.ToString("G17"), and you'll get 5.0149999999999997. So that's the number that you're multiplying by 100 and then rounding to 15 digits. Please see the links in my previous comment on April 22 for more details.
  • Eric Wood
    Eric Wood almost 3 years
    @PDaddy, Not sure if you want to handle these "bugs" in your code: 1) passing a negative number into "digits" parameter crashes it. 2) passing a number in the "digits" parameter that is larger than number of digits in the "number" parameter crashes it. (So like 346273.397586473754746, 234.) Crashes are System.ArgumentOutOfRangeException Rounding digits must be between 0 and 15, inclusive. from the System.Math.Round() function.
  • P Daddy
    P Daddy almost 3 years
    @EricWood: Thanks for doing some QA testing! But addressing these issues to make this function bulletproof is rather beyond the scope of an SO answer, IMO. My intention isn't to provide production-ready code, but to teach concepts. I feel that adding input validation would only distract from that goal.
  • ai_enabled
    ai_enabled over 2 years
    I would not recommend using this. Try to truncate double number 2.3 to two significant digits and you'll get only 2.2! That's because 2.3 is represented as 2.2999999999999998 due to insufficient double type precision, sadly.
  • P Daddy
    P Daddy over 2 years
    @ai_enabled: Your comment is correct about truncating floating point numbers, and is an important point for anybody who's considering doing so. Do note, though, that the wording used by the OP, and the first function I offered, is for rounding, not truncating. I only included a function to truncate because in the example the OP gave, his result was that of truncation. I think that was just a simple mistake on his part, and what he actually wanted was rounding, and that's what most people get from this answer, I think.
  • P Daddy
    P Daddy over 2 years
    @ai_enabled: Still, it is important to note the precision problem of floating point numbers, which can affect all sorts of calculations in unexpected ways. Please keep up the good fight.
  • ai_enabled
    ai_enabled over 2 years
    @PDaddy, you're correct, the rounding method works fine. I was in a rush and didn't test both methods as I needed truncation only. Truncating double numbers is always such a headache!
  • fero
    fero over 2 years
    This is definitely the best solution for my problem. I submitted 30/31 with a precision of 28 digits to an API, and the API confirmed it by returning a 16-digit value which didn't match my original value. To match the values, I'm now comparing submittedValue.ToString("G12") with returnedValue.ToString("G12") (which is enough precision in my case).