Round a double to x significant figures
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);
}
}
}
Related videos on Youtube
Rocco
Updated on July 05, 2022Comments
-
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 over 15 yearsI'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 over 15 yearsOr are you looking for N * 10^X, where N has a specified number of digits?
-
Alexander Aleksandrovič Klimov over 15 yearsPlease give us some more examples of original numbers and what you want to see as output
-
Rocco over 15 yearsRounding 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 over 15 yearsI 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 over 15 yearsNote, 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 over 15 yearsThat is correct, I was mistaken in thinking they are truncated.
-
strager over 15 yearsSimilar question : stackoverflow.com/questions/304011/… You can simply add round() in the appropriate place.
-
-
Rocco over 15 yearsIt 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 over 14 yearsMath.round(...) doesn't take two parameters
-
P Daddy over 14 years@leftbrainlogic: Yes, it really does: msdn.microsoft.com/en-us/library/75ks3aby.aspx
-
Andrew Hancox over 14 yearsFor some reason this code won't convert 50.846113537656557 to 6 sigfigs accurately, any ideas?
-
Fraser about 12 yearsNeither of these methods will work with negative numbers as Math.Log10 will return Double.NaN if d < 0.
-
P Daddy about 12 years@Fraser: Good catch. I'll leave it as an exercise for the reader to add
Math.Abs
. -
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 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 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 over 11 yearsFails for RoundToSignificantDigits(.00000000000000000846113537656557, 6) because Math.Round will not allow its second parameter to go beyond 15.
-
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 about 11 yearsOkay, edited to handle numbers ≤ 0. Note, though, that there remains one tiny inconsistency. If you call the
Round...
function with a value fordigits
that's outside the range [0, 15],Math.Round
will throw an argument exception. Ifd
is zero, thenMath.Round
isn't called, so there is no exception. Insignificant, really. Note that this doesn't apply to theTruncate...
function. -
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 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 about 10 years@PDaddy sorry for insufficient info, the numbers are inputs and the digits are 15
-
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 almost 10 yearsThat only gets you the number of significant digits to the right of the decimal point.
-
Gustav almost 9 yearsThat returns 2340.0004 - it least with some localisations.
-
u8it almost 8 yearsAlthough close, this doesn't always return sigfigs... for instance,
G4
would remove the zeros from1.000
-->1
. Also, it forces scientific notation at its discretion, whether you like it or not. -
farfareast over 7 yearsShould 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 over 7 yearsFails with (0.073699979, 7) returns
0.073699979999999998
-
Sнаđошƒаӽ over 7 yearsThe question is tagged C#
-
Derrick Moeller over 6 yearsI would argue, 1050 rounded to two significant digits is 1000. Round to even is a very common rounding method.
-
Ramakrishna Reddy about 4 yearsabove code producing the value 5.8999999999999995E-12 for 5.9E-12 in .Net Core 3.0
-
P Daddy about 4 years@RamakrishnaReddy: Please see questions like stackoverflow.com/questions/1089018 and stackoverflow.com/questions/18389455.
-
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 over 3 years@AmrAli: This is because
5.015
can't be represented exactly in IEEE 754 floating point representation. Try5.015.ToString("G17")
, and you'll get5.0149999999999997
. So that's the number that you're multiplying by100
and then rounding to 15 digits. Please see the links in my previous comment on April 22 for more details. -
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 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 over 2 yearsI 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 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 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 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 over 2 yearsThis 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")
withreturnedValue.ToString("G12")
(which is enough precision in my case).