Crossing the integer limits in PHP - php

Can someone please explain what is happening in the last 2 cases?
$x=PHP_INT_MAX;
var_dump($x); // int(9223372036854775807) no problem
var_dump($x+1); // float(9.2233720368548E+18) value is cast to float still no problem
var_dump($x+1-1); // float(9.2233720368548E+18) still okay
var_dump((int)($x+1-1)); // int(-9223372036854775808) negative value?!!
var_dump($x+1-$x); // float(0) zero?!!!!!!!!!!!
According to the PHP manual:
PHP_INT_MAX is the maximum integer size.
If PHP encounters a number beyond the bounds of the integer type, it will be interpreted as a float instead. Also, an operation which
results in a number beyond the bounds of the integer type will return
a float instead.
So the first 2 dumps are okay. The third dump gives the expected result, but why on earth does the last two dumps give a negative value and zero?

Two different things are combining: floating point rounding and integer overflow.
var_dump($x+1-$x); // float(0) zero?!!!!!!!!!!!
This is because floating point arithmetic is not exact (it can't be, as real numbers can be infinite). $x and $x+1 are so close together that they are rounded to the same floating point value, so float($x+1)==float($x). Now you know why $x+1-$x==0.
Combined with integer overflow you get this result:
var_dump((int)($x+1-1)); // int(-9223372036854775808) negative value?!!
Because of the reason above $x+1-1==float(9223372036854775808). When casting this to an int it overflows and becomes a negative value.
9223372036854775808==2^63, which in 64-bit signed integers becomes -2^63

About the 4th test:
Integers are signed. Internally they are stored in such a way, that when you add one, the value wraps and turns into the lowest negative integer. In case of a signed nibble of 4 bits, this would look like:
0111 (=7) + 0001 (=1) = 1000 (= -7)
This explains partially how a number can suddenly become a very large negative number.
But you would expect x+1-1 to be x, but it is probably an issue with significant digits. PHP stores only 14 significant digits, which is not enough to store a number like 9223372036854775807 exactly without losing information.
I'll explain using the 5th test:
A floating point can contain much bigger numbers than an integer, but only with a limited number of significant digits. That's why it looks like 9.2233720368548E+18 instead of 9223372036854800000 or 9223372036854775808.
Since low numbers like 1 are outside of the significant digits of the value, I think 9.2233720368548E+18 + 1 is still 9.2233720368548E+18. That also means that 9.2233720368548E+18 + 1 - 9.2233720368548E+18 is 0, because the +1 just didn't have any effect.
I think this is actually the explanation for both fenomena.
The weird thing is that you would normally not have to know about any of these implementation details, especially in a scripting language like PHP, but in tests like this, they may surface anyway and sometimes cause weird behavior.
Also, you might want to read The PHP floating point precision is wrong by default. It contains a good explanation of other rounding errors that might occur using floats, and it will give you a deeper understanding of how floats work in general and in PHP specifically.

This has to do with how the computer stores numbers. In this case, signed numbers, meaning they can both be negative and positive.
For the sake of it I will use an 8-bit example. The computer can store numbers from -128 to 127.
-128 is represented as 1000 0000 while 127 is presented as 0111 1111. The first bit represents if the number is negative or positive. Every bit is worth 2^n, were n is the position from right to left. For 127 it is 2^0 + 2^1 + 2^2 + 2^3 + 2^4 + 2^5 + 2^6 + 2^7 = 127. The opposite for a negative value.
var_dump($x); // int(9223372036854775807) no problem
No problems because we are just setting it to the maximum value.
var_dump($x+1); // float(9.2233720368548E+18) value is cast to float still no problem
var_dump($x+1-1); // float(9.2233720368548E+18) still okay
Here there are problems, +1 and +1-1 equals to the same thing. Which they should not, problem does occur here, a bit more subtle.
var_dump((int)($x+1-1)); // int(-9223372036854775808) negative value?!!
In this case 0111 (...) +1 equals to 1000 (...) which is the absolute value, notice the 8 in the end of the number, compared to the 7 for the positive value. Overflow flag in CPU is set.
var_dump($x+1-$x); // float(0) zero?!!!!!!!!!!!
$x+1 forces a cast to float, then subtracts the same value (because of rounding errors with float numbers), $x+1 is the same as $x, so $x-$x = 0.
Hope this clarifies some.

Related

Why strlen doesn't work in a paticular digit

I made this function. It seemed it's working but when it comes to 20 digits number, the return value was 19. I'm wondering why this problem happen..
My function
function sumDigits($n) {
return strlen($n);
}
echo sumDigits(100); //3
echo sumDigits(1000); //4
echo sumDigits(12345); //5
echo sumDigits(1000000000); //10
echo sumDigits(145874589632); //12
echo sumDigits(0); //1
echo sumDigits(12345698745254856320); //19 <-- Why not 20?
Can you please somebody explain for me?
Thank you so much.
First, I would point out that the name of your function is misleading, as you are not really summing the values of the digits, but are counting the digits. So I would call your function countDigits instead of sumDigits.
The reason why it doesn't work for large numbers, is that the string representation will switch to scientific notation, so you're actually getting the length of "1.2345698745255E+19" not of "12345698745254856320"
If you are only interested in integers, you will get better results with the logarithm:
function countDigits($n) {
return ceil(log10($n));
}
For numbers that have decimals, there is no good solution, since the precision of 64-bit floating pointing point numbers is limited to about 16 significant digits, so even if you provide more digits, the trailing decimals will be dropped -- this has nothing to do with your function, but with the precision of the number itself. For instance, you'll find that these two literals are equal:
if (1.123456789123456789123456789 == 1.12345678912345678) echo "equal";
Because you function parameter is an integer, exceeding the limit.
If you dump it, it actually shows the following:
1.2345698745255E+19 - which is 19 letters.
If you would do the following, it will return 20 - mind the quotes, which declares the input as string.
echo sumDigits("12345698745254856320"); //19 <-- Why not 20? -> now will be 20
As per documentation, strlen() expects a string so a cast happens. With default settings you get 1.2345698745255E+19:
var_dump((string)12345698745254856320);
string(19) "1.2345698745255E+19"
The root issue is that PHP converts your integer literal to float because it exceeds PHP_INT_MAX so it cannot be represented as integer:
var_dump(12345698745254856320, PHP_INT_MAX);
In 64-bit PHP:
float(1.2345698745254857E+19)
int(9223372036854775807)
You could change display settings to avoid E notation but you've already lost precision at this point.
Computer languages that store integers as a fixed amount of bytes do not allow arbitrary precision. Your best chance is to switch to strings:
var_dump('12345698745254856320', strlen('12345698745254856320'));
string(20) "12345698745254856320"
int(20)
... and optionally use an arbitrary precision library such as BCMath or GMP if you need actual maths.
It's also important to consider that this kind of issues is sometimes a symptom that your input data is not really meant to be an integer but just a very long digit-only string.

Why floor(0.99999999999999999) = 1 and floor(0.9999999999999999) = 0?

floor function in PHP behave weirdly.
For 16 decimal values it gives floor value but by increasing 1 decimal it round.
$int = 0.99999999999999999;
echo floor($int); // returns 1
$int = 0.9999999999999999;
echo floor($int); // returns 0
$int = 0.99999999999999994;
echo floor($int); // returns 0
Is it defined/explained somewhere, at which point it gives "round" value?
Is there any function which gives 0 anyhow how many 9 in decimals?
It's not floor that rounds, it's floating point math that does.
This line:
echo 0.99999999999999999;
Prints 1 (demo) because 0.99999999999999999 is too precise to be represented by a (64-bit?) float, so the closest possible value is taken, which happens to be 1.
0.99999999999999994 is also too precise to be represented exactly, but here the closest representable value happens to be 0.9999999999999999.
Is it defined/explained somewhere, at which point it gives "round" value?
It's complicated, but the numbers are rounded almost always.
I believe there is no definition of "from when values will be approximated", but that is a mathematical property that follows from the definitions in the IEEE 754 floating point standard.
To be safe, just assume everything is approximated.
Is there any function which gives 0 anyhow how many 9 in decimals?
No. The problem is that, for PHP, 0.99999999999999999 is literally the same as 1.
They're represented by exactly the same sequence of bits, so it can't distinguish them.
There are some solutions to work with bigger precision decimals, but that requires some major code changes.
Probably of interest to you:
Working with large numbers in PHP
Note that while you may get arbitrary precision, you will never get infinite precision, as that would require infinite amounts of storage.
Also note that if you actually were dealing with infinite precision, 0.999... (going on forever) would be truly (as in, mathematically provable) equal to 1, as explained in depth in this Wikipedia article.
$float_14_digits = 0.99999999999999;
echo $float_14_digits; // prints 0.99999999999999
echo floor($float_14_digits); // prints 0
$float_15_digits = 0.999999999999999;
echo $float_15_digits; // prints 1
echo floor($float_15_digits); // prints 1
exit;
on my development machine that behavior happens on digit '15' not '17' like yours. PHP rounds the last digit in the floating numbers. your floor() function has nothing to do with this behavior

Is this floating point behavior or a bug in PHP?

CLARIFYING: This isn't asking why I'm getting rounding errors. I understand this is a mistake or an oversight. The question asks why it prints as whole in the first var_dump, but casting acts as if it were 57916.9repeating and truncates said .9repeating.
The following occurs:
You take a string (or float -- does not matter) that contains the value 579.17 and multiply it 100. It var_dumps the expected 57917. Not 57916.99999999999999999999999 or similar. var_dump should not be rounding anything as a debugging function in my opinion. It may have to truncate, but rounding is unexpected in a debugging function.
However, if one then casts that to an integer, you get an unexpected 57916 from var_dump.
I'm aware of issues with floating point numbers, but the act of casting a floating point number that prints as exactly 57917 in PHP apparently effectively subtracts 1. This is a very small number.
This only appears to happen for some numbers, such as 579.17. It does not occur for others I've tested. All we're doing is multiplying a number by 100 to send to an API that expects cents. The API library understandably casts to integer since the API doesn't accept fractional cents.
Test case:
php -r '$n = ("579.17" * 100); var_dump($n, (int)$n);'
Output:
float(57917)
int(57916)
Environment:
x86-32,
x86-64 both.
var_dump uses precision from php.ini to display float value. You could raise it to see what happens.
php -r 'ini_set("precision", 20); $n = ("579.17" * 100); var_dump($n, (int)$n);'
// double(57916.999999999992724)
// int(57916)
Also. There is no matter x86 or x64. PHP uses 64 bits for floats.
http://php.net/manual/en/language.types.float.php
Use round() instead of int(). The actual value of 579.17 * 100 is something like 57916.99999. var_dump() shows this as 57917, but when you use int() it truncates the fraction. Using round() will go to the nearest integer, rather than always truncating down.
I believe this is because hardware cannot truly and accurately express floating point numbers. So what appears as 579.17 is actually more like 579.16999999. So when you multiply it and cast it as an int it truncates the decimal leaving you with 57916.

php - why does floor round down a integer?

I am confused as to why:
echo log10(238328) / log10(62);
results in 3
but
echo floor(log10(238328) / log10(62));
results in 2
I know floor rounds down but I thought it was only for decimal numbers.
How can I get an answer of 3 out of the latter statment whilst still normally rounding down?
PHP uses double-precision floating point numbers. Neither of the results of the two logarithms can be represented exactly, so the result of dividing them is not exact. The result you get is close to, but slightly less than 3. This gets rounded to 3 when being formatted by echo. floor, however returns 2.
You can avoid the inexact division by taking advantage of the fact that log(x, b) / log(y, b) is equivalent to log(x, y) (for any base b). This gives you the the expression log(238328, 62) instead, which has a floating point result of exactly 3 (the correct result since 238328 is pow(62, 3)).
It's due to the way floating point numbers are polished in PHP.
See the PHP Manual's Floating Point Numbers entry for more info
A workaround is to floor(round($value, 15));. Doing this will ensure that your number is polished quite accurately.
If you var_dump you'll see that the "3" is actually a float. Which means its probably close to 3 and rounded up. If you wanted 3, you would have to use the sister function, ceil.
You might get better results using the round() function and/or explicitly casting it to an int rather than relying on ceil(). Look here for more information: http://php.net/manual/en/language.types.integer.php
At the cost of a little performance, you could coerce it, reducing the precision to a more useful range by rounding or string formatting the number:
echo floor(round(log10(238328)/log10(62), 4));
echo floor(sprintf('%.4f', log10(238328)/log10(62)));
// output:
// 3
// 3
You should go with the minimum precision that you need. More precision is not what you want. Rounding without flooring might be more correct, the results are different depending on precision.
echo floor(round(log10(238328)/log10(62), 16));
echo round(log10(238328)/log10(62), 16);
// output:
// 2
// 3
there three functions for doing nearly the same:
ceil --> ceil(0.2)==1 && ceil(0.8)==1
floor --> floor(0.2)==0 && floor(0.8)==0
round --> round(0.2)==0 && round(0.8)==1

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);

Categories