21

I want to shorten a number to the first significant digit that is not 0. The digits behind should be rounded.

Examples:

0.001 -> 0.001
0.00367 -> 0.004
0.00337 -> 0.003
0.000000564 -> 0.0000006
0.00000432907543029 ->  0.000004

Currently I have the following procedure:

if (value < (decimal) 0.01)
{
    value = Math.Round(value, 4);
}

Note:

  • numbers will always be positive
  • the number of significant digits will always be 1
  • values larger 0.01 will always be rounded to two decimal places, hence the if < 0.01

As you can see from the examples above, a rounding to 4 Decimal places might not be enough and the value might vary greatly.

D-Shih
  • 44,943
  • 6
  • 31
  • 51
julian bechtold
  • 1,875
  • 2
  • 19
  • 49
  • 6
    Have a look here: [round-a-double-to-x-significant-figures](https://stackoverflow.com/questions/374316/round-a-double-to-x-significant-figures) – Aaron Hayman Sep 21 '18 at 09:50
  • 2
    @AaronHayman: This is `decimal` rather than `double`, and the number of significant digits isn't necessarily known - I suspect the OP doesn't want to round 123.456 to 100 for example. (Although we'll see...) – Jon Skeet Sep 21 '18 at 09:51
  • 4
    What do you want to do for values that aren't between -1 and 1? For example, what would the result for 123.456 be? – Jon Skeet Sep 21 '18 at 09:52
  • The number of significant figures is surely one, from the description. I didn't say the link is the answer, but it does provides useful information to solving the problem – Aaron Hayman Sep 21 '18 at 09:54
  • 1
    I was expecting some solution that uses `GetBits` to be faster, but (it seems that) the mantissa are not necessarily normalized, it's no easier than finding number of decimal places of the mantissa (still, some bit-scan-reverse hack may work) – user202729 Sep 21 '18 at 11:01
  • 1
    Possible duplicate of [Round a double to x significant figures](https://stackoverflow.com/questions/374316/round-a-double-to-x-significant-figures) – mmmmmm Sep 21 '18 at 13:32

5 Answers5

21

I would declare precision variable and use a iteration multiplies that variable by 10 with the original value it didn't hit, that precision will add 1.

then use precision variable be Math.Round second parameter.

I had added some modifications to the method which can support not only zero decimal points but also all decimal numbers.

static decimal RoundFirstSignificantDigit(decimal input) {

    if(input == 0)
       return input;

    int precision = 0;
    var val = input - Math.Round(input,0);
    while (Math.Abs(val) < 1)
    {
        val *= 10;
        precision++;
    }
    return Math.Round(input, precision);
}

I would write an extension method for this function.

public static class FloatExtension
{
    public static decimal RoundFirstSignificantDigit(this decimal input)
    {
        if(input == 0)
            return input;

        int precision = 0;
        var val = input - Math.Round(input,0);
        while (Math.Abs(val) < 1)
        {
            val *= 10;
            precision++;
        }
        return Math.Round(input, precision);
    }
}   

then use like

decimal input = 0.00001;
input.RoundFirstSignificantDigit();

c# online

Result

(-0.001m).RoundFirstSignificantDigit()                  -0.001
(-0.00367m).RoundFirstSignificantDigit()                -0.004
(0.000000564m).RoundFirstSignificantDigit()             0.0000006
(0.00000432907543029m).RoundFirstSignificantDigit()     0.000004
D-Shih
  • 44,943
  • 6
  • 31
  • 51
2

Something like that ?

    public decimal SpecialRound(decimal value) 
    {
        int posDot = value.ToString().IndexOf('.'); // Maybe use something about cultural (in Fr it's ",")
        if(posDot == -1)
            return value;

        int posFirstNumber = value.ToString().IndexOfAny(new char[9] {'1', '2', '3', '4', '5', '6', '7', '8', '9'}, posDot);

        return Math.Round(value, posFirstNumber);
    }
Benjamin K
  • 296
  • 1
  • 2
  • 15
1
var value = 0.000000564;

int cnt = 0;
bool hitNum = false;
var tempVal = value;
while (!hitNum)
{
    if(tempVal > 1)
    {
        hitNum = true;
    }
    else
    {
        tempVal *= 10;
        cnt++;
    }
}

var newValue = (decimal)Math.Round(value, cnt);
ManishM
  • 583
  • 5
  • 7
1

code is from R but the algo should be obvious

> x = 0.0004932
> y = log10(x)
> z = ceiling(y)
> a = round(10^(y-z),1)
> finally = a*10^(z)
> finally
[1] 5e-04

the following was basically already provided by Benjamin K

At the risk of being labelled a complete wacko, I would cheerfully announce that regexp is your friend. Convert your number to a char string, search for the location of the first char that is neither "." nor "0" , grab the char at that location and the next char behind it, convert them to a number, round, and (because you were careful), multiply the result by $10^{-(number of zeros you found between "." and the first number)}$

Carl Witthoft
  • 20,573
  • 9
  • 43
  • 73
0

Another approach

    decimal RoundToFirstNonNullDecimal(decimal value)
    {
        var nullDecimals = value.ToString().Split('.').LastOrDefault()?.TakeWhile(c => c == '0').Count();
        var roundTo = nullDecimals.HasValue && nullDecimals >= 1 ? nullDecimals.Value + 1 : 2;
        return Math.Round(value, roundTo);
    }

Result

        Console.WriteLine(RoundToFirstNonNullDecimal(0.001m));                0.001
        Console.WriteLine(RoundToFirstNonNullDecimal(0.00367m));              0.004
        Console.WriteLine(RoundToFirstNonNullDecimal(0.000000564m));          0.0000006
        Console.WriteLine(RoundToFirstNonNullDecimal(0.00000432907543029m));  0.000004
        Console.WriteLine(RoundToFirstNonNullDecimal(0.12m));                 0.12
        Console.WriteLine(RoundToFirstNonNullDecimal(1.232m));                1.23
        Console.WriteLine(RoundToFirstNonNullDecimal(7));                     7.00
crazy_p
  • 240
  • 3
  • 6
  • Your results don't match the criteria of "values larger 0.01 will always be rounded to two decimal places". The last two examples should be 1.23 and 7.00 – Glen Yates Sep 21 '18 at 17:14
  • you're right @GlenYates, for some reason I missed that requirement, I've edited my answer now.However this is a way slower solution than the accepted one, which I also upvoted! – crazy_p Sep 23 '18 at 21:08