PHP - Converting a String float to an Integer - Wrong value - php

Why does this code provide 853 instead of 854?
(int) ((float) ( "8.54") * 100);
How do I convert (string) "8.54" into (integer) 854?

First of all, read Is floating point math broken?
8.54 happens to be one of those numbers which can be written precisely in decimal, but not in binary, so (float)"8.54" will actually create a number very very close to, but slightly under, 8.54.
Multiplying that by 100 will create a number very very close to, but slightly under, 854. And casting to int will find the next smallest integer (e.g. 853.9 becomes 853, not 854).
I can think of two solutions:
After multiplying by 100, round to the nearest whole number, then convert to integer. Unlike 8.54, 854 itself can be represented precisely in floating point, so round(8.54 * 100) will give a number that is exactly 854, rather than slightly under it. So (int)round((float)"8.54" * 100) will give 854 as an integer.
Instead of multiplying by 100, remove the . from the string, and convert to int directly, e.g. (int)str_replace(".", "", "8.54"));

intval(str_replace(".", "", "8.54"))

Try without (int).
Like this => (float) ( "8.54") * 100;
In this case (int) not necessary.

Related

PHP cast float to string is different from cast to int

Something you would not expect but need to be aware of when you're dealing with floating point numbers in php
<?php
$i = (32.87*100);
echo $i; // outputs 3287
echo (int) $i; // outputs 3286 !!
echo (int) (string) $i // outputs 3287
Internal representation of $i is something like 3286.9999999.
Why is the string representation of $i 3287 ?
Let's go through your code:
$i = (32.87*100);
Now $i is slightly less than 3287 as float as shown below:
echo sprintf('%.30f', $i) . PHP_EOL; //3286.999999999999545252649113535881
But when you print (echo) it, you'll get rounded value.
echo $i; // outputs 3287
And here we come to the trick - casting float to int means to simply cut off the part after dot, despite its .99999999(...) which is almost 1 (but it's not!). So the output is 3286.
echo (int) $i; // outputs 3286 !!
Now, in the last example, you first cast float to string, which means exactly what you already did by doing echo $i; because whatever you print, internally PHP need to cast to string. So it's 3286.999999999999545252649113535881 casted to "3287" and then casted to 3287, and then printed.
echo (int) (string) $i // outputs 3287;
To sum up, it's difference between the way float is casted to string and int.
EDIT Further explanation about "rounding"
Well it's not really rounding. I've made a mistake by saying that.
PHP uses 64 bit float (do called double), which in decimal representation has 14 digit precision.
As mentioned in PHP manual:
The size of a float is platform-dependent, although a maximum of approximately 1.8e308 with a precision of roughly 14 decimal digits is a common value (the 64 bit IEEE format).
That means, that a float can contain (for most of the time) a 14-digit number (in decimal) and it doesn't matter where the dot is placed.
Now, the most important thing:
Casting to string doesn't round the float number
Examples:
$a = 1.23456789012349 - the last 9 is 15th digit, so you'll get "rounded" float to 1.2345678901235
$a = 12345678901234.9 - same as above
$a = 1.99999999999999 - last 9 is 15th digit, so you'll get 2
And as a string it will be printed exactly as the float is, which means 14 digits precision. The "rounding" is at the moment when we create float variable's structure in memory.
The last example is what we're talking about in this topic.
Now, why I did that mistake and said about "rounding"?
I misunderstood the result of echo sprintf('%.30f', $i). A saw many more digits and thought it's the real value of the float number.
But it's not.
As we know, 64-bit float has only 14 digits precision.
So where the result of sprintf comes from?
The answer is actually pretty easy.
We already know that it's not always possible to express a decimal number in binary system. So for example a simple 0.1 in float (binary representation) is just an approximation because the real binary representation would be infinitely long.
Now it works exactly the same when converting binary system to decimal. What can be expressed in binary (which means every float value), not always is possible to express in decimal.
So what sprintf('%.30f', $i) is to give the 30-digit precision approximation of converting the float number from binary to decimal system.
Thanks to #Quasimodo'sclone for asking in comment for being more precise about this. That made me go a little deeper in this topic.
You're casting $i (3287) to a string and then to an int, so the result stays 3287.
If you cast $i to an int you'll get 3286, and then if you cast it to a string you'll have what you want.
Try echo (string) (int) $i

PHP casting float to int returns different value

When I'm executing the following code in PHP (v5.5.9) something unexpected happens:
$valueAsCents = 54780 / 100 * 100;
var_dump($valueAsCents);
var_dump((int) $valueAsCents);
This returns
float 54780
int 54779
So apparently the float value with no decimals, is not equal to the int value. Any ideas of what's going on here?
When you divide $valueAsCents = 54780 / 100 then it becomes a float which is not always accurate in digital form because of the way they are stored. In my tests I got
547.7999999999999545252649113535881042480468750000
When multiplied by 100 this is would be
54779.9999999999927240423858165740966796870000
When PHP casts to int, it always rounds down.
When converting from float to integer, the number will be rounded towards zero.
This is why the int value is 54779
Additionally, the PHP manual for float type also includes a hint that floating point numbers may not do what you expect.
Additionally, rational numbers that are exactly representable as floating point numbers in base 10, like 0.1 or 0.7, do not have an exact representation as floating point numbers in base 2, which is used internally, no matter the size of the mantissa. Hence, they cannot be converted into their internal binary counterparts without a small loss of precision. This can lead to confusing results: for example, floor((0.1+0.7)*10) will usually return 7 instead of the expected 8, since the internal representation will be something like 7.9999999999999991118....

Sanitize currency input

I'm working with currency input. Only two digits after decimal mark should be used. I tried casting input to float and multiplying by 100, which works fine until someone enters more than two digits after decimal mark:
// Returns 6999.8 instead of 6999
$cents = floatval('69.998') * 100;
Then I tried casting result to int, so sequential digits after decimal point are ignored. It solves above problem ('69.998' becomes 6999), but creates a new one with float to integer conversion:
// Returns 6998 instead of 6999
$cents = intval(floatval('69.99') * 100);
I also considered floor(), but it triggers the same float issue as intval().
This is what I'm thinking about using:
$cents = intval((string)(floatval('69.99') * 100));
It works in both cases, but feels like a hack and it's late and my head hurts so maybe I'm missing something obvious here. Is there a better way to do this?
Is
$cents = intval(round(floatval('69.99') * 100));
what you need?
You can also specify the precision. For example, in your case you mentioned you would like to round the original to two decimal places:
$twodecimal = round(floatval('69.998'),2);//returns a float representation of 70
Be sure to have a look at the big red notice in these docs
It's because 69.99 * 100 has a floating-point representation of 6998.9999999* (off: you can check it at a javascript console too). If you want to be precise, you should use a fixed-point number with a php-extension, like BCMath - or, you can write a simple regexp for this specific problem
$amount = '69.99';
if (preg_match('/^(-?\d+)(\.(\d{1,2}))?/', $amount, $matches))
{
$amount = (int) ($matches[1] . (isset($matches[3]) ? str_pad($matches[3], 2, '0') : '00'));
}
else
{
$amount = ((int) $amount) * 100;
}
$cents = intval(round(floatval('69.99') * 100));
This would get it to the nearest number correctly, this is because of floating pointer precision problems as 69.99 is probably represented in memory to be something like 69.9899999
intval just truncates the remaining parts of the decimal, so 69.989999 * 100 becomes 6998.9999 and gets truncated to 6998
I would recommend that you use an integer to contain a currency value. Using floats can rapidly lead to rounding errors.
In the applications that I have seen, an integer is used with an assumed decimal point. All values are held to the nearest unit of currency such as the cent for US dollars or the Euro. There are currencies which do not have a decimal point and there are a couple that rather than two decimal places have three decimal places.
By manipulating with integers with an assumed decimal place, you can really reduce rounding errors and other issues that can be seen with floating point.
To do conversion, I recommend using string manipulation and removing the decimal point with string manipulation as well as performing a check to ensure that only the desired number of places are entered.

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