Rounding to an arbitrary number of significant digits

SUMMARY:

double roundit(double num, double N)
{
    double d = log10(num);
    double power;
    if (num > 0)
    {
        d = ceil(d);
        power = -(d-N);
    }
    else
    {
        d = floor(d); 
        power = -(d-N);
    }

    return (int)(num * pow(10.0, power) + 0.5) * pow(10.0, -power);
}

So you need to find the decimal place of the first non-zero digit, then save the next N-1 digits, then round the Nth digit based on the rest.

We can use log to do the first.

log 1239451 = 6.09
log 12.1257 = 1.08
log 0.0681  = -1.16

So for numbers > 0, take the ceil of the log. For numbers < 0, take the floor of the log.

Now we have the digit d: 7 in the first case, 2 in the 2nd, -2 in the 3rd.

We have to round the (d-N)th digit. Something like:

double roundedrest = num * pow(10, -(d-N));

pow(1239451, -4) = 123.9451
pow(12.1257, 1)  = 121.257
pow(0.0681, 4)   = 681

Then do the standard rounding thing:

roundedrest = (int)(roundedrest + 0.5);

And undo the pow.

roundednum = pow(roundedrest, -(power))

Where power is the power calculated above.


About accuracy: Pyrolistical's answer is indeed closer to the real result. But note that you can't represent 12.1 exactly in any case. If you print the answers as follows:

System.out.println(new BigDecimal(n));

The answers are:

Pyro's: 12.0999999999999996447286321199499070644378662109375
Mine: 12.10000000000000142108547152020037174224853515625
Printing 12.1 directly: 12.0999999999999996447286321199499070644378662109375

So, use Pyro's answer!


Here's a short and sweet JavaScript implementation:

function sigFigs(n, sig) {
    var mult = Math.pow(10, sig - Math.floor(Math.log(n) / Math.LN10) - 1);
    return Math.round(n * mult) / mult;
}

alert(sigFigs(1234567, 3)); // Gives 1230000
alert(sigFigs(0.06805, 3)); // Gives 0.0681
alert(sigFigs(5, 3)); // Gives 5

Here's the same code in Java without the 12.100000000000001 bug other answers have

I also removed repeated code, changed power to a type integer to prevent floating issues when n - d is done, and made the long intermediate more clear

The bug was caused by multiplying a large number with a small number. Instead I divide two numbers of similar size.

EDIT
Fixed more bugs. Added check for 0 as it would result in NaN. Made the function actually work with negative numbers (The original code doesn't handle negative numbers because a log of a negative number is a complex number)

public static double roundToSignificantFigures(double num, int n) {
    if(num == 0) {
        return 0;
    }

    final double d = Math.ceil(Math.log10(num < 0 ? -num: num));
    final int power = n - (int) d;

    final double magnitude = Math.pow(10, power);
    final long shifted = Math.round(num*magnitude);
    return shifted/magnitude;
}