21

When a float number needs to be truncated to a certain digit after the floating point, it turns out that it is not easy to be done. For example if the truncating has to be done to second digit after the point, the numbers should be

45.8976 => 45.89, 0.0185 => 0.01

( second digit after the point is not rounded according to the third digit after the point ).

Functions like round(), number_format(), sprintf() round the number and print out

45.8976 => 45.90, 0.0185 => 0.02

I have met two solutions and I am wondering if they are good enough and which one is better to be used

1.

function truncNumber( $number, $prec = 2 )
{
    return bccomp( $number, 0, 10 ) == 0 ? $number : round( $number - pow( 0.1, bcadd(   $prec, 1 ) ) * 5, $prec );
}

2.

function truncNumber($number, $prec = 2 )
{
    return sprintf( "%.".$prec."f", floor( $number*pow( 10, $prec ) )/pow( 10, $prec ) );
}
Nik
  • 2,885
  • 2
  • 25
  • 25
Dessislava Mitova
  • 371
  • 2
  • 3
  • 8
  • 1
    hi. where did you get this function from? have they worked correctly for you? which one did you chooose & why ? – arod Apr 03 '13 at 15:25

14 Answers14

28

floor will do as you ask.

floor(45.8976 * 100) / 100;

You won't find a more direct function for this, since it's a kind of odd thing to ask. Normally you'll round mathematically correct. Out of curiosity - What do you need this for?

troelskn
  • 115,121
  • 27
  • 131
  • 155
  • 3
    Yes, that is probably the best solution to truncate positive float numbers. To work with negative floats, abs() of the number should be taken and the sign added after that.It is a formula coming from the business that if it produces too long float number, all digits after the second should be just ignored, instead of rounded:). – Dessislava Mitova Jan 14 '11 at 07:58
  • 3
    if we already have two digits after floating point, but didn't know about it and try with your code: `echo floor(10.04 * 100) / 100;` will return `10.03`. – jamapag Jul 31 '12 at 08:50
  • 2
    Same as jampag. echo floor(4.60 * 100) / 100; will return 4.59 which is wrong. – Jimmy Jan 15 '14 at 18:07
  • Same years after I figured a new problem with truncating negative numbers `floor(-3.1)` will result in `-4`. And `floor(-45.8976 * 100) / 100` will result in `-45.9`instead `-45.89` – Ragen Dazs Mar 10 '23 at 12:42
21

this is my solution:

/**
 * @example truncate(-1.49999, 2); // returns -1.49
 * @example truncate(.49999, 3); // returns 0.499
 * @param float $val
 * @param int f
 * @return float
 */
function truncate($val, $f="0")
{
    if(($p = strpos($val, '.')) !== false) {
        $val = floatval(substr($val, 0, $p + 1 + $f));
    }
    return $val;
}
Ragen Dazs
  • 2,115
  • 3
  • 28
  • 56
  • It's right. I really doesn't remember why I had to add abs() in this this function – Ragen Dazs Oct 06 '12 at 11:38
  • so having something we don't know if it is necessary or not can not be *"the simplest"* on my eyes (no offence please, just saying you should probably reword that a little and remove the non-necessary parts from your answer to get better feedback) – hakre Oct 06 '12 at 11:40
  • 1
    commented code is not helpful. you never understand why the code is commented. it's either not needed (then it would be removed) or it is needed (then it wouldn't be commented). It's really bad to have commented code blocks, use version control instead (like on this site, there are revisions of your answer). Also double check your use of the `strpos` function. your usage looks wrong to me, for example if there is no `.`. Check with the manual, it's a common error if you do not spot it directly. – hakre Oct 06 '12 at 11:54
  • By the way, it's really have a problem using "if" with strpos() in case of numbers like truncate(.49999, 3) - it's because strpos('.49999', '.') returns 0 and without using the explicit comparison with !== false the function will have an abnormal return. Thanks again for the feedback – Ragen Dazs Oct 06 '12 at 12:27
  • exactly, that's what I was concerned about. looks much better now! congratulations to your first answer on Stackoverflow by the way. – hakre Oct 06 '12 at 12:32
  • Fails for very small values. `0.000001` gives `1.0` when it should be `0.0` – BARNZ Jun 18 '20 at 21:55
  • @BARNZ it's really sucks, but the solution in this case can be pass that value as a string "0.000001" or increase ini_set('precision', 7); – Ragen Dazs Jun 19 '20 at 18:36
  • Yeah. Doing an `$val = sprintf('%f', $val)` first seems to do the trick to remove the exponential form. – BARNZ Jun 21 '20 at 19:05
11
function truncate($value)
{
    if ($value < 0)
    {
        return ceil((string)($value * 100)) / 100;
    }
    else
    {
        return floor((string)($value * 100)) / 100;
    }
}

Why does PHP not handle mathamatic formulas the way we would expect, e.g. floor(10.04 * 100) = 1003 when we all know it should be 1004. This issue is not just in PHP but can be found in all langauges, depending on the relative error in the langauge used. PHP uses IEEE 754 double precision format which has a relative error of about 1.11e-16. (resource)

The real issue is that the floor function casts the float value into an int value, e.g. (int)(10.04 * 100) = 1003 as we see in the floor function earlier. (resource)

So to overcome this issue we can cast the float to a string, a string can represent any value accurately, then the floor function will cast the string to an int accurately.

Community
  • 1
  • 1
justinrza
  • 409
  • 5
  • 4
6

To truncate numbers the "best" is to use (I took a number here that works for my example):

$number = 120,321314;

$truncate_number  = number_format($number , 1); // 120,3
$truncate_number  = number_format($number , 2); // 120,32
$truncate_number  = number_format($number , 3); // 120,321

Hope this help is easy than other answers here, but it is easy only for the cases it works for. Here is a case it does not work for (demo):

   $number = 10.046;

    echo number_format($number , 2); // 10.05

The number_format function is tricky, you can solve your problem this way (from php.net):

To prevent the rounding that occurs when next digit after last significant decimal is 5 (mentioned by several people below):

<?php

function fnumber_format($number, $decimals='', $sep1='', $sep2='') {

        if (($number * pow(10 , $decimals + 1) % 10 ) == 5)
            $number -= pow(10 , -($decimals+1));

        return number_format($number, $decimals, $sep1, $sep2);
}

$t=7.15;
echo $t . " | " . number_format($t, 1, '.', ',') .  " | " . fnumber_format($t, 1, '.', ',') . "\n\n";
//result is: 7.15 | 7.2 | 7.1

$t=7.3215;
echo $t . " | " . number_format($t, 3, '.', ',') .  " | " . fnumber_format($t, 3, '.', ',') . "\n\n";
//result is: 7.3215 | 7.322 | 7.321
} ?>
sandino
  • 3,813
  • 1
  • 19
  • 24
  • 5
    if $number will be 10.046, your code will return 10.05, not 10.04. – jamapag Jul 31 '12 at 08:28
  • You are right guys and missed that behaviour but I honestly think the behaviour is strange, I mean "number_format" the name of this function make you think is like formating the display only moving the comma. – sandino Oct 21 '12 at 06:18
4

To do this accurately for both +ve and -ve numbers you need use:
- the php floor() function for +ve numbers
- the php ceil() function for -ve numbers

function truncate_float($number, $decimals) {
    $power = pow(10, $decimals); 
    if($number > 0){
        return floor($number * $power) / $power; 
    } else {
        return ceil($number * $power) / $power; 
    }
}

the reason for this is that floor() always rounds the number down, not towards zero.
ie floor() effectively rounds -ve numbers towards a larger absolute value
eg floor(1.5) = 1 while floor(-1.5) = 2

Therefore for the truncate method using multiply by power, round, then divide by power:
- floor() only works for positive numbers
- ceil() only works for negative numbers

To test this, copy the following code into the editor of http://phpfiddle.org/lite (or similar):

<div>Php Truncate Function</div>
<br>
<?php
    function truncate_float($number, $places) {
        $power = pow(10, $places); 
        if($number > 0){
            return floor($number * $power) / $power; 
        } else {
            return ceil($number * $power) / $power; 
        }
    }

    // demo
    $lat = 52.4884;
    $lng = -1.88651;
    $lat_tr = truncate_float($lat, 3);
    $lng_tr = truncate_float($lng, 3);
    echo 'lat = ' . $lat . '<br>';
    echo 'lat truncated = ' . $lat_tr . '<br>';
    echo 'lat = ' . $lng . '<br>';
    echo 'lat truncated = ' . $lng_tr . '<br><br>';

    // demo of floor() on negatives
    echo 'floor (1.5) = ' . floor(1.5) . '<br>';
    echo 'floor (-1.5) = ' . floor(-1.5) . '<br>';
?>
goredwards
  • 2,486
  • 2
  • 30
  • 40
3

Though this question is old, I'll post another answer in case someone stumbles upon this problem like I did. A simple solution for positive numbers is:

function truncate($x, $digits) { 
  return round($x - 5 * pow(10, -($digits + 1)), $digits); 
}

I tested it against a string-based solution with 10 million random fractions and they always matched. It doesn't exhibit the precision issue that floor-based solutions do.

Extending it for for zero and negatives is quite simple.

Juan
  • 53
  • 4
2

I'm seeing another method to perform this:

function trunc($float, $prec = 2) {
    return substr(round($float, $prec+1), 0, -1);
}

But it's not better than any other... round can be replaced with sprintf too.

Savageman
  • 9,257
  • 6
  • 40
  • 50
1

The reason for this is the internal binary representation of floating-point numbers. The given value from above 10.04 has to represented internally as a power of the base 2.

The potential exponent for the floating-point part is ln(0,04)/ln(2) = -4,64.... This already shows that 10.04 cannot be represented exactly as a binary number. We end up having something like 10.04 ~ 2^3 + 2^1 + 2^-5 + 2^-7 + 2^-11 + ... with having a maximum precision for the floating-point part.

This is the reason that 10.04 is internally actually a bit less and might be represented as 10.039....

To work around this behavior there are two possibilities - either fiddle around directly with string operations or use an arbitrary precision library like BcMath for PHP.

<?php
function test($value)
{
    // direct string operations
    $fromString = (float)preg_replace(
        '#(?:(\.\d{1,2})\d*)?$#',
        '${1}',
        (string)$value
    );
    // using arbitrary precision
    // @see http://php.net/manual/en/function.bcadd.php
    $fromBcMath = (float)bcadd(
        (string)$value,
        '0',
        2
    );

    var_dump($fromString, $fromBcMath);
}

test(3);
test(4.60);
test(10.04);
test(45.8976);

The output for the code above will generate (I added separating newlines for readability):

double(3)
double(3)

double(4.6)
double(4.6)

double(10.04)
double(10.04)

double(45.89)
double(45.89)
Oliver Hader
  • 4,093
  • 1
  • 25
  • 47
1

The round() function does have a precision paramter as well as a third parameter for specifying the rounding method, but you're right, it doesn't do truncating.

What you're looking for are the floor() and ceil() functions. The downside is that they don't have a precision parameter, so you'll have to do something like this:

$truncated = floor($value*100)/100;
Spudley
  • 166,037
  • 39
  • 233
  • 307
0

I would like to add my contribute to this answer with the function I use for my needs, that considers truncate positions both for negative and positive values.

function truncate($val,$p = 0)
{
  $strpos = strpos($val, '.');
  return $p < 0 ? round($val,$p) : ($strpos ? (float)substr($val,0,$strpos+1+$p) : $val);
}

...I think it's simple and fast to use.

Nineoclick
  • 804
  • 9
  • 17
0

To overcome the problem with floor for positives and ceil for negatives, you can just divide the number by one and get the integer part using intdiv(). And the float problem can be solved by using round() after the multiplication.

So, this should work:

<?php
function truncate($number, $precision = 2) {
    $prec = 10 ** $precision;
    return intdiv(round($number * $prec, PHP_ROUND_HALF_DOWN), 1) / $prec;
}

Repl.it

Outputs:

>>> truncate(45.8976)
=> 45.89
>>> truncate(-45.8976)
=> -45.89
>>> truncate(3)
=> 3
>>> truncate(-3)
=> -3
>>> truncate(10.04)
=> 10.04
>>> truncate(-10.04)
=> -10.04
fernandosavio
  • 9,849
  • 4
  • 24
  • 34
0
function truncNumber($number, $places){
 $strN = preg_split("/\./", (string)$number);
 $num = $strN[0];
 $frac = $strN[1];
 $num .= ($places > 0) ? ".".substr($frac, 0, $places): "";
 return (double)$num;
}
Dharman
  • 30,962
  • 25
  • 85
  • 135
AExplosion
  • 51
  • 2
-1

You can try this:

bcdiv($inputNumber, '1', $precision = 2);
Dino
  • 7,779
  • 12
  • 46
  • 85
-1

In case you want to implement similar function in Js you can use the following:

    function truncate_float($number, $decimals) {
        $power = Math.pow(10, $decimals); 
        if($number > 0){
            return Math.floor($number * $power) / $power; 
        } else {
            return Math.ceil($number * $power) / $power; 
        }
    }

Example:

 function truncate_float($number, $decimals) {
        $power = Math.pow(10, $decimals); 
        if($number > 0){
            return Math.floor($number * $power) / $power; 
        } else {
            return Math.ceil($number * $power) / $power; 
        }
 }
 
 var orig_float = 0.38583929928947889839483;
 var the_float = truncate_float(orig_float,2);
 
 document.getElementById("orig").innerText = orig_float;
 document.getElementById("trunc").innerText = the_float;
Original: <span id="orig"></span> <br>
Truncated: <span id="trunc"></span>
Dimitrios Desyllas
  • 9,082
  • 15
  • 74
  • 164