1

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?

Jesse
  • 6,725
  • 5
  • 40
  • 45
  • Probably round() help in your case? – Vladimir Gilevich Aug 24 '15 at 09:23
  • @VladimirGilevich I don't see how that's related. Can you be more specific? – Jesse Aug 24 '15 at 09:25
  • Those extra decimal places may be useless, but they are [very much real](http://stackoverflow.com/questions/588004/is-floating-point-math-broken). – Ignacio Vazquez-Abrams Aug 24 '15 at 09:52
  • @Jesse, just first what I thinking was round(), but probably it is not better variant. But if you add next code in your loop before "$decimals++;": `$rounded = floor($float * pow(10, $decimals)) / pow(10, $decimals); if ($rounded > 0 && $result == $rounded) { break;}` Probably it will give you closer results to that you need... – Vladimir Gilevich Aug 24 '15 at 10:15
  • @IgnacioVazquez-Abrams I know they're real. – Jesse Aug 24 '15 at 12:58

3 Answers3

1

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.

Simon Byrne
  • 7,694
  • 1
  • 26
  • 50
  • Does my brute force little loop correctly do this "very complicated endeavour"? Or is simply that all the more efficient ways are much more complicated? – Jesse Aug 25 '15 at 01:05
  • I was perhaps a bit brusk: your methods does actually do it correctly (albeit inefficiently, but this is probably not a huge problem unless you're repeating it millions of times). What you shouldn't do is what Vladimir suggested: multiply or divide by powers of 10, then round, as this introduces floating point rounding error. – Simon Byrne Aug 25 '15 at 11:11
1

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.

Hexydec
  • 21
  • 2
0

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.

Jesse
  • 6,725
  • 5
  • 40
  • 45