Float to int error - php

Yesterday I was helping some one and got a weird error which I could not explain to him how it worked.
The code (tested on 3 machines (PHP 5.3 and PHP 5.4))
// (float)65.35
$percentage = round(65.351, 2);
// (float) 6535
$total = $percentage * 100;
// (int) 6534
$int = (int) $total;
What is suspected was that the int value would be 6535 but it ended up being 6534.
Could some one explain what is going on?

You don't actually have 65.35 after the first operation.
>>> '%.20f' % (65.351 - 0.001,)
'65.34999999999999431566'
Either start with an integral value scaled appropriately in the first place, don't attempt to convert the value to an integer, or add a small value before taking the integer value.

This has to do with how floating point (read the warning in this link!) values are stored in memory. Indeed after the first operation you don't have an exact decimal value, but a rounded value. Probably 65.34999999 or so. (The value is stored as a list of bits (0/1))
This is why when talking about money, developers don't store dollars/euros but rather the amount of cents. This way they avoid working with floats that are less precise for decimals, but rather work with integers, that are precise.

Use round instead of int
round($total)

$r=$explode('.',$total);
debug($r);

Related

Round to nearest whole number PHP

I figure this is easy enough but I am missing something here. Im using Stripe Connect and trying to calculate an application fee (in cents). The problem is my application fee sometimes has a decimal so it is throwing an error. I have tried using round() which gives me and ceil() but Im still getting a decimal and trailing zero in my answer so it returns an error.
$payment = bcmul($request->amount, 100); //112.00 - Convert to cents for stripe becomes 11200
$applicationFee = $payment * 0.021; //235.2 but should be just 235
print_r($applicationFee); //Should be whole number, round and ceil still provide me with decimal number IE 235.0 instead of 235
How can I make sure applicationFee is always a whole number with no decimal and rounded to nearest whole number?
You are correct as is described in the docs for ceil:
The return value of ceil() is still of type float
The method explanation also tells you that a float is returned:
float ceil ( float $value )
The return type for round() is also shown as a float in the docs for round as below:
float round ( float $val [, int $precision = 0 [, int $mode = PHP_ROUND_HALF_UP ]] )
You could use intval() instead as below:
intval($applicationFee);
Or try casting to an int as below:
(int) $applicationFee
If you want to keep your ceil or rounding you can call these first and then cast it as below:
intval(ceil($applicationFee));
(int) ceil($applicationFee);
This should work! (using round() function)
Documentation: Round()
$payment = bcmul($request->amount, 100);
$applicationFee = $payment * 0.021;
print_r(round($applicationFee));
round() should do the trick
$applicationFee = round($payment * 0.021);
You could add more parameters to round and so allow some decimals sill present in the result.
You say you tried it, but I wonder how.
Note
21 is is some countries the VAT percentage (taxes). Be careful how you round those numbers, as the tax office can be strickt
question: according to the docs round() returns a float, as it can also round to 1 or more decimals. But if the 2nd parameter is omitted you should get a whole number. How does a float equal to 235.0 influence the rest of the script?
Php automatically assigns a data type when you don't specifically tell it. For this to work you have to assign a data type that allows you to do the calculations you are trying to do.
Insure you dimension them with enough space for your calculations.
Then as a last step round it off to the precision that you desire and print it out.
It is highly unlikely that if you let PHP select the data type that you will get what you want. You have to carefully select for PHP what data type your variables are instantiated as.

Simple math with decimals in PHP

This is killing me! I've never had so much trouble and I can't figure out what I'm doing wrong here.
If I have a number, say 2.32, and I want to do math with it it won't work out. The very simplest example:
$income = $commission; //Commission is 2.32, retrieved from XML
echo "income: $income<br>";
$income100 = $income*100;
echo "income100: $income100<br>";
The result I get is:
income: 2.32
income100: 200
How can I use a decimal number accurately with math without it changing it?
Thanks so much!
You need to assign $income in the following manner to get rid of the underlying SimpleXMLElement:
$income = (float) $commission;
Example of what happens when you don't:
$x = simplexml_load_string("<a>2.4</a>");
echo $x * 100; // output: 200
Besides using floats as Tim said, also make sure to use the BC Math functions when performing arithmetic operation on floating point numbers. Specifically bcmul():
$income100 = bcmul($income, 100);
The problem with floating-point numbers is that you cannot represent decimal numbers with them (unless it can be written as a/b for integer a and b, and even then only if abs(a) < pow(2,52) and b is a power of 2).
You may be better off using string functions to get an integer value:
$tmp = explode(".",$commission);
$tmp = intval($tmp[0].str_pad(substr($tmp[1],0,2),2,"0"));
This will split up the integer part from the decimal part, ensure the decima part is two digits long, and shove it on the end of the integer part, thus effectively multiplying the original number by 100.
I think the easiest solution would be to cast it to a float with floatval()
$income = floatval($comission)
leave the rest of the code as is and it should work as intended.

PHP ceil gives wrong results if input is a float with no decimal

I've been wrestling with PHP's ceil() function giving me slightly wrong results - consider the following:
$num = 2.7*3; //float(8.1)
$num*=10; //float(81)
$num = ceil($num); //82, but shouldn't this be 81??
$num/=10; //float(8.2)
I have a number which may have any number of decimal places, and I need it rounded up to one decimal place.
i.e 8.1 should be 8.1, 8.154 should be 8.2, and 8 should be left as 8.
How I've been getting there is to take the number, multiply by 10, ceil() it, then divide by ten but as you can see I'm getting an extra .1 added in some circumstances.
Can anyone tell my why this is happening, and how to fix it?
Any help greatly appreciated
EDIT: had +=10 instead of *=10 :S
EDIT 2:
I didn't explicitly mention this but I need the decimal to ALWAYS round UP, never down - this answer is closest so far:
rtrim(rtrim(sprintf('%.1f', $num), '0'), '.');
However rounds 3.84 down to 3.8 when I need 3.9.
Sorry this wasn't clearer :(
Final Edit:
What I ended up doing was this:
$num = 2.7*3; //float(8.1)
$num*=10; //float(81)
$num = ceil(round($num, 2)); //81 :)
$num/=10; //float(8.1)
Which works :)
This is more than likely due to floating point error.
http://support.microsoft.com/kb/42980
http://download.oracle.com/docs/cd/E19957-01/806-3568/ncg_goldberg.html
http://joshblog.net/2007/01/30/flash-floating-point-number-errors/
http://en.wikipedia.org/wiki/Floating_point
You may have luck trying this procedure instead.
<?php
$num = 2.7*3;
echo rtrim(rtrim(sprintf('%.1f', $num), '0'), '.');
Floats can be a fickle thing. Not all real numbers can be properly represented in a finite number of binary bits.
As it turns out, a decimal section of 0.7 is one of those numbers (comes out 0.10 with an infinity repeating "1100" after it). You end up with a number that's ever so slightly above 0.7, so when you multiply by 10, you have a one's digit slightly above 7.
What you can do is make a sanity check. Take you float digit and subtract it's integer form. If the resulting value is less than, say, 0.0001, consider it to be an internal rounding error and leave it as-is. If the result is greater than 0.0001, apply ceil() normally.
Edit: A fun example you can do if you're on windows to show this is to open up the built in calculator application. Put in "4" then apply a square root function (with x^y where y=0.5). You'll see it properly displays "2". Now, subtract 2 from it and you'll see that you don't have 0 as a result. This is caused by internal rounding errors when it attempted to compute the square root of 4. When displaying the number 2 earlier, it knew that those very distant trailing digits were probably a rounding error, but when those are all that's left, it gets a bit confused.
(Before anybody gets onto me about this, I understand that this is oversimplified, but nonetheless I consider it a decent example.)
Convert your number to a string and ceil the string.
function roundUp($number, $decimalPlaces){
$multi = pow(10, $decimalPlaces);
$nrAsStr = ($number * $multi) . "";
return ceil($nrAsStr) / $multi;
}
The problem is that floating point numbers are RARELY what you expect them to be. Your 2.7*3 is probably coming out to be something like 81.0000000000000000001, which ceil()'s up to 82. For this sort of thing, you'll have to wrap your ceil/round/floor calls with some precision checks, to handle those extra microscopic differences.
Use %f instead of %.1f.
echo rtrim(rtrim(sprintf('%f', $num), '0'), '.');
Why not try this:
$num = 2.7*3;
$num *= 100;
$num = floor($num);
$num /= 10;
$num = ceil($num);
$num /= 10;

php intval() and floor() return value that is too low?

Because the float data type in PHP is inaccurate, and a FLOAT in MySQL takes up more space than an INT (and is inaccurate), I always store prices as INTs, multipling by 100 before storing to ensure we have exactly 2 decimal places of precision. However I believe PHP is misbehaving. Example code:
echo "<pre>";
$price = "1.15";
echo "Price = ";
var_dump($price);
$price_corrected = $price*100;
echo "Corrected price = ";
var_dump($price_corrected);
$price_int = intval(floor($price_corrected));
echo "Integer price = ";
var_dump($price_int);
echo "</pre>";
Produced output:
Price = string(4) "1.15"
Corrected price = float(115)
Integer price = int(114)
I was surprised. When the final result was lower than expected by 1, I was expecting the output of my test to look more like:
Price = string(4) "1.15"
Corrected price = float(114.999999999)
Integer price = int(114)
which would demonstrate the inaccuracy of the float type. But why is floor(115) returning 114??
Try this as a quick fix:
$price_int = intval(floor($price_corrected + 0.5));
The problem you are experiencing is not PHP's fault, all programming languages using real numbers with floating point arithmetics have similar issues.
The general rule of thumb for monetary calculations is to never use floats (neither in the database nor in your script). You can avoid all kinds of problems by always storing the cents instead of dollars. The cents are integers, and you can freely add them together, and multiply by other integers. Whenever you display the number, make sure you insert a dot in front of the last two digits.
The reason why you are getting 114 instead of 115 is that floor rounds down, towards the nearest integer, thus floor(114.999999999) becomes 114. The more interesting question is why 1.15 * 100 is 114.999999999 instead of 115. The reason for that is that 1.15 is not exactly 115/100, but it is a very little less, so if you multiply by 100, you get a number a tiny bit smaller than 115.
Here is a more detailed explanation what echo 1.15 * 100; does:
It parses 1.15 to a binary floating point number. This involves rounding, it happens to round down a little bit to get the binary floating point number nearest to 1.15. The reason why you cannot get an exact number (without rounding error) is that 1.15 has infinite number of numerals in base 2.
It parses 100 to a binary floating point number. This involves rounding, but since 100 is a small integer, the rounding error is zero.
It computes the product of the previous two numbers. This also involves a little rounding, to find the nearest binary floating point number. The rounding error happens to be zero in this operation.
It converts the binary floating point number to a base 10 decimal number with a dot, and prints this representation. This also involves a little rounding.
The reason why PHP prints the surprising Corrected price = float(115) (instead of 114.999...) is that var_dump doesn't print the exact number (!), but it prints the number rounded to n - 2 (or n - 1) digits, where n digits is the precision of the calculation. You can easily verify this:
echo 1.15 * 100; # this prints 115
printf("%.30f", 1.15 * 100); # you 114.999....
echo 1.15 * 100 == 115.0 ? "same" : "different"; # this prints `different'
echo 1.15 * 100 < 115.0 ? "less" : "not-less"; # this prints `less'
If you are printing floats, remember: you don't always see all digits when you print the float.
See also the big warning near the beginning of the PHP float docs.
The other answers have covered the cause and a good workaround to the problem, I believe.
To aim at fixing the problem from a different angle:
For storing price values in MySQL, you should probably look at the DECIMAL type, which lets you store exact values with decimal places.
Maybe it's another possible solution for this "problem":
intval(number_format($problematic_float, 0, '', ''));
PHP is doing rounding based on significant digits. It's hiding the inaccuracy (on line 2). Of course, when floor comes along, it doesn't know any better and lops it all the way down.
As stated this is not a problem with PHP per se, It is more of an issue of handling fractions that can't be expressed as finite floating point values hence leading to loss of character when rounding up.
The solution is to ensure that when you are working on floating point values and you need to maintain accuracy - use the gmp functions or the BC maths functions - bcpow, bcmul et al. and the problem will be resolved easily.
E.g instead of
$price_corrected = $price*100;
use $price_corrected = bcmul($price,100);

PHP money string conversion to integer error

I have a small financial application with PHP as the front end and MySQL as the back end. I have ancient prejudices, and I store money values in MySQL as an integer of cents. My HTML forms allow input of dollar values, like "156.64" and I use PHP to convert that to cents and then I store the cents in the database.
I have a function that both cleans the dollar value from the form, and converts it to cents. I strip leading text, I strip trailing text, I multiply by 100 and convert to an integer. That final step is
$cents = (integer) ($dollars * 100);
This works fine for almost everything, except for a very few values like '156.64' which consistently converts to 15663 cents. Why does it do this?
If I do this:
$cents = (integer) ($dollars * 100 + 0.5);
then it consistently works. Why do I need to add that rounding value?
Also, my prejudices about storing money amounts as integers and not floating point values, is that no longer needed? Will modern float calculations produce nicely rounded and accurate money values adequate for keeping 100% accurate accounting?
If you want precision, you should store your money values using the DECIMAL data type in MySQL.
Your "prejudices" about floats will never be overcome - it's fundamental to the way they work. Without going into too much detail, they store a number based on powers of two and since not all decimal number can be presented this way, it doesn't always work. Your only reliable solution is to store the number as a sequence of digits and the location of the decimal point (as per DECIMAL type mentioned above).
I'm not 100% on the PHP, but is it possible the multiplication is converting the ints to floats and hence introducing exactly the problem you're trying to avoid?
Currency/money values should never be stored in a database (or used in a program) as floats.
Your integer method is fine, as is using a DECIMAL, NUMERIC or MONEY type where available.
Your problem is caused by $dollars being treated as a float and PHP doesn't have a better type to deal with money. Depending on when $dollars is being assigned, it could be being treated as a string or a float, but is certainly converted to a float if it's still a string for the * 100 operation if it looks like a float.
You might be better off parsing the string to an integer "money" value yourself (using a regex) instead of relying on the implicit conversions which PHP is doing.
The code you posted does the multiplication first, forcing a floating point calculation that introduces error, before converting the value to an integer. Instead, you should avoid floating point arithmetic entirely by reversing the order. Convert to integer values first, then perform the arithmetic.
Assuming previous code already validated and formatted the input, try this:
list($bills, $pennies) = explode('.', $dollars);
$cents = 100 * $bills + $pennies;
Your prejudice against floating point values to represent money is well founded because of truncation and because of values being converted from base-10 to base-2 and back again.
Casting does not round() as in round-to-nearest, it truncates at the decimal: (int)3.99 yields 3. (int)-3.99 yields -3.
Since float arithmetic often induces error (and possibly not in the direction you want), use round() if you want reliable rounding.
You should never ever store currency in floating point, because it always get results you don't expect.
Check out php BC Maths, it allow you to store your currency as string, then perform very high precision arithmetic on them.
Instead of using
$cents = (integer) ($dollars * 100);
you may want to try to use:
$cents = bcmul($dollars, 100, 2);
When converting from float to integer, the number will be rounded towards zero (src).
Read the Floating point precision warning.
There's no point in storing money as integer if you enter it through a floating point operation (no pun intended). If you want to convert from string to int and be consistent with your "prejudice" you can simply use string functions.
You can use an arbitrary precision library to divide by 10 (they handle numbers internally as strings), e.g. bcdiv() or gmp_div_q(), but of course, you could have also used it from the beginning for all the math.
Or you can use plain string functions:
<?php
// Quick ugly code not fully tested
$input = '156.64';
$output = NULL;
if( preg_match('/\d+(\.\d+)?/', $input) ){
$tmp = explode('.', $input);
switch( count($tmp) ){
case 1:
$output = $tmp[0];
break;
case 2:
$output = $tmp[0] . substr($tmp[1], 0, 2);
break;
default:
echo "Invalid decimal\n";
}
}else{
echo "Invalid number\n";
}
var_dump($output);
?>

Categories