Format a float with minimum required number of decimal places

2.2k Views Asked by At

I want to format a float with the minimum number of decimal places required to reproduce it.

PHP has a number_format() function for rendering a number with a specified number of decimal places. However, if I use it to format 0.1 with a very high number of decimals, I get:

print rtrim(number_format(0.1, 1000, '.', ''), '0');
// 0.1000000000000000055511151231257827021181583404541015625

Since (float)"0.1" === 0.1, those extra 55511151... decimals after position 16 are useless.

I can use a loop, like this:

function format_float($float) {
    $decimals = 1;
    do {
        $result = number_format($float, $decimals, '.', '');
        $decimals++;
    } while ((float)$result !== $float);
    return $result;
}

print format_float(0.1) . "\n"; // 0.1
print format_float(1/3) . "\n"; // 0.3333333333333333
print format_float(1E-50) . "\n"; // 0.00000000000000000000000000000000000000000000000001

But surely there is a simpler and more efficient way?

3

There are 3 best solutions below

0
Jesse On BEST ANSWER

This is what I came up with:

function format_float($num) {
    $dec = $num == 0 ? 0 : ceil(-log10(abs($num)));
    $dec = max(1, $dec + 15 /* magic number */);
    $res = number_format($num, $dec, '.', '');

    // sometimes we need one more decimal
    if ((float)$res !== $num) {
        $res = number_format($num, $dec + 1, '.', '');
    }

    list($l, $r) = explode('.', $res, 2);
    return "$l." . (rtrim($r) ?: '0');
}

It assumes the number of decimals needed will be 15 - log10($num) or 16 - log10($num), which seems to hold in practice according to my testing. It's at least more efficient than my brute force loop.

2
Simon Byrne On

Correctly printing the minimal number of decimal digits of a binary floating point number is a very complicated endeavour. The current state-of-the-art are the grisu family of algorithms. For a good explanation of the problems involved, see the classic paper by Steele and White.

0
Hexydec On

Code from @Jesse didn't work for me, so build this:

function formatFloat(float $num, int $dec = 0) : string {

    // format to maximum decimal places, and make positive
    $abs = number_format(abs($num), 17, '.', '');

    // is it less than 0?
    if (!floor($abs)) {

        // look through each number until we find one that is not 0
        foreach (str_split(substr($abs, 2)) AS $i => $item) {
            if ($item) {
                break;
            }
        }

        // add defined decimal places
        $dec += $i + 1;
    }

    // format the output
    return number_format($num, $dec);
}

It can handle numbers larger than one, negative numbers, and enables you to specify how many decimal places to format the float to after the first significant number is found. It also performs better.