Search code examples
phpfloating-pointconditional-statementsnumber-formattingdecimal-point

How to round, format, and conditionally trim zeros from floats?


I have written a custom function which must call number_format() to round my input number (float) to predetermined number of decimal places (the number of decimal places should cap at 5) and make modifications to the decimal character and the thousands delimiter.

After the number is rounded and formatted, I also wish to conditionally trim some of the trailing zeros from the decimal value.

Specifically, if the input value is to be rounded to 0, 1, or 2 decimal places, then no zero trimming is necessary. However, if the rounding is to 3 or more places, then trailing zeros can be removed until only two decimal places remain.

Here is a matrix of test cases and my desired output:

                |      D    E    C    I    M    A    L      P    L    A    C    E    S   
     INPUT      |   0   |    1    |     2    |     3     |     4      |      5      |       6
----------------+-------+---------+----------+-----------+------------+-------------+------------
1.01            |     1 |     1,0 |     1,01 |     1,01  |     1,01   |     1,01    |     1,01
----------------+-------+---------+----------+-----------+------------+-------------+------------
1.044           |     1 |     1,0 |     1,04 |     1,044 |     1,044  |     1,044   |     1,044
----------------+-------+---------+----------+-----------+------------+-------------+------------
0.240000        |     0 |     0,2 |     0,24 |     0,24  |     0,24   |     0,24    |     0,24
----------------+-------+---------+----------+-----------+------------+-------------+------------
0.200           |     0 |     0,2 |     0,20 |     0,20  |     0,20   |     0,20    |     0,20
----------------+-------+---------+----------+-----------+------------+-------------+------------
0.24540         |     0 |     0,2 |     0,25 |     0,245 |     0,2454 |     0,2454  |     0,2454
----------------+-------+---------+----------+-----------+------------+-------------+------------
0.24546         |     0 |     0,2 |     0,25 |     0,245 |     0,2455 |     0,24546 |     0,24546
----------------+-------+---------+----------+-----------+------------+-------------+------------
6500.00         | 6 500 | 6 500,0 | 6 500,00 | 6 500,00  | 6 500,00   | 6 500,00    | 6 500,00
----------------+-------+---------+----------+-----------+------------+-------------+------------
3760.54         | 3 761 | 3 760,5 | 3 760,54 | 3 760,54  | 3 760,54   | 3 760,54    | 3 760,54
----------------+-------+---------+----------+-----------+------------+-------------+------------
3760.000000000  | 3 760 | 3 760,0 | 3 760,00 | 3 760,00  | 3 760,00   | 3 760,00    | 3 760,00
----------------+-------+---------+----------+-----------+------------+-------------+------------
0.04000         |     0 |     0,0 |     0,04 |     0,04  |     0,04   |     0,04    |     0,04
----------------+-------+---------+----------+-----------+------------+-------------+------------
1.000000        |     1 |     1,0 |     1,00 |     1,00  |     1,00   |     1,00    |     1,00
----------------+-------+---------+----------+-----------+------------+-------------+------------
1.000005        |     1 |     1,0 |     1,00 |     1,00  |     1,00   |     1,00001 |     1,00001

This is my current code:

function format($number, $decimal=0, $thousands=' ')
{
    $number = number_format($number, $decimal, '.', '');

    $whole    = floor($number);
    $fraction = $number - $whole;

    if(!(int)$fraction && $decimal > 2) {
        $decimal = 2;
    }

    if (false !== strpos($number, '.')) {
        $number = rtrim(rtrim($number, '0'), '.');
    }

    $number = number_format($number, $decimal, '.', $thousands);

    return $number;
}

Solution

  • I have rewritten my solution and confirmed it to be as accurate as your posted solution with the sample strings provided.

    If my solution fails in your project, please provide new sample data that will expose the issue.

    Rules, as I understand them:

    1. number_format() MUST be the first manipulation on $number so that rounding is performed and output accuracy is maintained.
    2. You have a hard-coded upper limit on decimal length which is 5.
    3. When the passed $decimals parameter is greater than 2, it is acceptable for the trailing zeros to be removed until there are only two digits remaining.

    Note, because the output from number_format() will be a string that cannot be processed as a number AND because the trimming process requires several steps to assess and execute, I'll employ a single regex pattern to reduce code bloat.

    Code: (Demo)

    function format($number, $decimals = 2, $dec_point = ',', $thousands_sep = ' ') {
        $number = number_format($number, min(5, $decimals), $dec_point, $thousands_sep);  // round to decimal max
        return preg_replace("~".preg_quote($dec_point, '~')."\d{".min(2, $decimals)."}[^0]*\K0+$~", '', $number);  // trim zeros (if any, beyond minimum allowance) from right side of decimal character
    }
    

    Of course, you can pull out the min() and preg_quote() calls if you wish to declare single-use variables; or alternatively, if you like freakishly long one-liners:

    function format($number, $decimals = 2, $dec_point = ',', $thousands_sep = ' ') {
        return preg_replace("~".preg_quote($dec_point, '~')."\d{".min(2, $decimals)."}[^0]*\K0+$~", '', number_format($number, min(5, $decimals), $dec_point, $thousands_sep));
    }
    

    Pattern Breakdown:

    "                       #double-quote-wrap the expression to allow variables to be parsed
    ~                       #pattern delimiter
    ".preg_quote($dec_point, '~')."  #match the (escaped when necessary) value of `$dec_point`match the value declared as $dec_point
    \d{".min(2, $decimals)."}        #match any two digits (if $decimals<2 then use $decimals else use 2 as "quantifier)
    [^0]*                   #match zero or more non-zero characters (could have also used [1-9])
    \K                      #restart the fullstring match (release/forget all previously matched characters
    0+                      #match one or more occurrences of the character: 0
    $                       #match the end of the string
    ~                       #pattern delimiter
    "                       #close double-quoted expression
    

    As per the desired effect, this pattern will only remove trailing zeros where appropriate/applicable. It will not add zeros to the right side of the string.