5

I have a System.Decimal number

0.00123456789

and I wish to round to 3 significant figures. I expect

0.00123

with the behaviour to be a rounding behaviour rather than truncation. Is there a bullet proof way to do this in .Net?

JJJ
  • 32,902
  • 20
  • 89
  • 102
bradgonesurfing
  • 30,949
  • 17
  • 114
  • 217
  • 2
    Possible duplicate of: http://stackoverflow.com/questions/158172/formatting-numbers-with-significant-figures-in-c-sharp. This question has a great explanation which may help. – Martin Aug 09 '13 at 09:58
  • Those answers all deal with double. I was curious if there is a specific technique for base 10 (decimal) numbers which should be easier to deal with. – bradgonesurfing Aug 09 '13 at 10:00
  • 1
    But the answer given there does work. – bradgonesurfing Aug 09 '13 at 10:08
  • You will first have to write a version of Math.Log10() that works for Decimal. That's extremely painful. Converting it to a string and then counting off digits is ugly but not painful. – Hans Passant Aug 09 '13 at 11:43
  • @HansPassant Considering that then on top of the Math.Log10 a Math.Ceiling is done, and that double have a bigger range (but smaller precision), could there be an error using the Math.Log10(double)? And if yes, where should it be searched? – xanatos Aug 09 '13 at 12:12
  • The double version isn't accurate enough for decimal. If you don't mind sometimes getting one too many or few digits when the value is close to a power of 10 then it is okay. People tend to mind, it is an ugly entry in the bug database that will nag you for a very long time. – Hans Passant Aug 09 '13 at 12:17

4 Answers4

5

You can try this... But I don't guarantee anything... Written and tested in 20 minutes and based on Pyrolistical's code from https://stackoverflow.com/a/1581007/613130 There is a big difference in that he uses a long for the shifted variable (because a double has a precision of 15-16 digits, while a long has 18-19, so a long is enough), while I use a decimal (because decimal has a precision of 28-29 digits).

public static decimal RoundToSignificantFigures(decimal num, int n)
{
    if (num == 0)
    {
        return 0;
    }

    // We are only looking for the next power of 10... 
    // The double conversion could impact in some corner cases,
    // but I'm not able to construct them...
    int d = (int)Math.Ceiling(Math.Log10((double)Math.Abs(num)));
    int power = n - d;

    // Same here, Math.Pow(10, *) is an integer number
    decimal magnitude = (decimal)Math.Pow(10, power);

    // I'm using the MidpointRounding.AwayFromZero . I'm not sure
    // having a MidpointRounding.ToEven would be useful (is Banker's
    // rounding used for significant figures?)
    decimal shifted = Math.Round(num * magnitude, 0, MidpointRounding.AwayFromZero);
    decimal ret = shifted / magnitude;

    return ret;
}

If you don't trust the (int)Math.Ceiling(Math.Log10((double) you could use this:

private static readonly decimal[] Pows = Enumerable.Range(-28, 57)
    .Select(p => (decimal)Math.Pow(10, p))
    .ToArray();

public static int Log10Ceiling(decimal num)
{
    int log10 = Array.BinarySearch(Pows, num);
    return (log10 >= 0 ? log10 : ~log10) - 28;
}

I have written it in another 20 minutes (and yes, I have tested all the Math.Pow((double), p) for all the values -28 - +28). It seems to work, and it's only 20% slower than the C# formula based on doubles). It's based on a static array of pows and a BinarySearch. Luckily the BinarySearch already "suggests" the next element when it can't find one :-), so the Ceiling is for free.

xanatos
  • 109,618
  • 12
  • 197
  • 280
0

in example:

decimal a = 1.9999M;
decimal b = Math.Round(a, 2); //returns 2
0

SqlDecimal has fast methods to calculate and adjust precision.

public static decimal RoundToSignificantFigures(decimal num, int n)
{
    SqlDecimal value = New SqlDecimal(num);
    if (value.Precision > num){
        int digits = num - (value.Precision - value.Scale);
        value = SqlDecimal.Round(value, digits);
        value = SqlDecimal.AdjustScale(value, (digits>0 ? digits : 0) - dstValue.Scale, True);
    }
    return value.Value;
}
Jeremy Lakeman
  • 139
  • 1
  • 1
-1

try this ... decimalVar.ToString ("#.##");