-11

I want the simplest thing to calculate:

$example = 90000000000009132 - 1;
echo sprintf('%.0f',  $example);

Surprisingly I get:

           90000000000009136

my question is - How can I get correct answer in php for such operation? Can you give me a correct code?

p.s. After searching the net, I've found explanations, saying that "this is not a bug" and some myths about float's and so on... Well, I won't start proving why I think that this really is an obvious bug and that we just don't care what happens in the background of PC, but the obvious thing is that we dont get correct number when subtracting X from Y, so it's definitely mistake! (But please, don't argue with me on this, because my question is different - clearly written above).

p.s.After being this question anomaly donwvoted, I couldn't find real solution in referred links. None of them worked, at least on my hosting. So, all most of those "answers" claimed to be the solution, seems useless on some hostings.

T.Todua
  • 53,146
  • 19
  • 236
  • 237
  • 4
    As a sign of respect for your reputation, I humbly suggest [`bcsub()`](http://php.net/manual/en/function.bcsub.php). The exact reasoning behind how this has eluded you for this long is a little worrying especially if you've needed precise calculations in the past. – MonkeyZeus Jul 02 '18 at 17:20
  • 4
    [It's *not* an really obvious bug](https://stackoverflow.com/questions/588004/is-floating-point-math-broken) – John Conde Jul 02 '18 at 17:22
  • `echo PHP_INT_MAX;` – AbraCadaver Jul 02 '18 at 17:27
  • `echo serialize(90000000000009132);` – AbraCadaver Jul 02 '18 at 17:30
  • 2
    What happens if you print before subtracting 1? If that also gives a different (higher) number than 90000000000009132 it would allow to narrow the problem down and simplify the question. – Yunnosch Jul 02 '18 at 17:44
  • @T.Todua Have you read the comments in http://php.net/manual/en/language.types.float.php ? There are tons of workarounds... – Fusseldieb Jul 02 '18 at 17:47
  • I am afraid that probably means that some of the comments (especially on floating point representation), which you reject, are in fact - true. – Yunnosch Jul 02 '18 at 17:51
  • Try adding 1 to that higher number. I predict that the result remains the same. If my prediction is true, will you listen to my explanation? – Yunnosch Jul 02 '18 at 17:53
  • 5
    Floating point numbers cannot represent every possible number exaclty, instead only about 2^64 of them. `90000000000009131` is not one of them. The closest alternative is used. Code needs to use a different type. – chux - Reinstate Monica Jul 02 '18 at 17:53
  • If both of my predictions are true and you ask me to, I will explain my thinking. I cannot tell you how to fix it (coming from a different programming language). My understanding however indicates that you will find the solution among the comments of other people talking about floating point representation. – Yunnosch Jul 02 '18 at 17:57
  • @Yunnosch can you please post your comments in combined answer? i should upvote that, as you have been the most thoughtful and adequately answered on this topic (even thought if you dont have a solution). thanks! – T.Todua Jul 02 '18 at 17:58
  • @chux Thanks ! please post it as answer, i will upvote, as it's good answer. – T.Todua Jul 02 '18 at 17:59
  • What about my prediction that adding 1 to that hihger number does not change it? Can you confirm? If not then I'd have to adapt my thinking to your results, which puts me in a weaker position. – Yunnosch Jul 02 '18 at 18:00
  • I can see the wrong expectation, yet cannot provide a PHP answer - so its only the first part of an answer. – chux - Reinstate Monica Jul 02 '18 at 18:00
  • @chux yes, and what you say this all, can you post it as answer? btw, and your advise, **what i should do in this case** ? – T.Todua Jul 02 '18 at 18:06
  • I am not fluent enough in PHP for a quality answer. Best advice I can provide: [use a different type](https://stackoverflow.com/questions/51140939/do-arithmetic-operations-on-float-numbers-correctly?noredirect=1#comment89266926_51140939). Note: you post your own answer. – chux - Reinstate Monica Jul 02 '18 at 18:08

2 Answers2

4

Sure the problem of PHP floating point precision was a topic on SO quite often, still I cannot see why this questions albeits 18 downvotes as the questionare asked very precisly for a concrete problem with a concrete floating point number. I have solved this in a way that everybody should understand clearly what the problem here is (not just those unprecise reasoning).

Here we go - see the results of the following:

$example = 90000000000009132 - 1;

var_dump(PHP_INT_MAX);
var_dump($example); 
var_dump(PHP_FLOAT_MAX);
var_dump(sprintf('%.0g',  $example)); 
var_dump(sprintf('%.0f',  $example));

The results:

int(9223372036854775807) 
int(90000000000009131) 
float(1.7976931348623E+308) 
string(7) "9.0e+16" 
string(17) "90000000000009136"

That means:

  • You are not above the maximum integer size which would make arithmetic operations fail
  • The result of your calculation is correct with integers
  • You are not above PHP_FLOAT_MAX (requires PHP 7.2) though the size of this depends on your environment
  • The floating point representation of your calculation is unexpectedly wrong (as your printf statement converts to float and then to string)

So why do we have this strange result? Lets make some tests:

var_dump(sprintf('%.0f',  90000000000009051));
var_dump(sprintf('%.0f',  90000000000009101));
var_dump(sprintf('%.0f',  90000000000009105));
var_dump(sprintf('%.0f',  90000000000009114));
var_dump(sprintf('%.0f',  90000000000009121));
var_dump(sprintf('%.0f',  90000000000009124));
var_dump(sprintf('%.0f',  90000000000009128));
var_dump(sprintf('%.0f',  90000000000009130));
var_dump(sprintf('%.0f',  90000000000009131));
var_dump(sprintf('%.0f',  90000000000009138));
var_dump(sprintf('%.0f',  90000000000009142));
var_dump(sprintf('%.0f',  90000000000009177));

The result of this is:

string(17) "90000000000009056"
string(17) "90000000000009104"
string(17) "90000000000009104"
string(17) "90000000000009120"
string(17) "90000000000009120"
string(17) "90000000000009120"
string(17) "90000000000009120"
string(17) "90000000000009136"
string(17) "90000000000009136"
string(17) "90000000000009136"
string(17) "90000000000009136"
string(17) "90000000000009184"

Look at those numbers. The digits at the end are always dividable by 16. And what means the 16 in information technology? A byte. Hexadecimal. What does it tell us? You probably have exceeded the limit where floats are basically precise (not necessarily in arithmetic operations).

What if we reduce those numbers by one zero to 16 digits?

var_dump(sprintf('%.0f',  9000000000009051));
var_dump(sprintf('%.0f',  9000000000009101));
var_dump(sprintf('%.0f',  9000000000009124));
var_dump(sprintf('%.0f',  9000000000009138));
var_dump(sprintf('%.0f',  9000000000009142));
var_dump(sprintf('%.0f',  9000000000009177));

...

string(16) "9000000000009051"
string(16) "9000000000009101"
string(16) "9000000000009124"
string(16) "9000000000009138"
string(16) "9000000000009142"
string(16) "9000000000009177"

The output is always correct. Well. And what if we improve it by one zero digit?

var_dump(sprintf('%.0f',  900000000000009051));
var_dump(sprintf('%.0f',  900000000000009101));
var_dump(sprintf('%.0f',  900000000000009124));
var_dump(sprintf('%.0f',  900000000000009138));
var_dump(sprintf('%.0f',  900000000000009142));
var_dump(sprintf('%.0f',  900000000000009177));

...

string(18) "900000000000009088"
string(18) "900000000000009088"
string(18) "900000000000009088"
string(18) "900000000000009088"
string(18) "900000000000009088"
string(18) "900000000000009216"

Interestingly this are factors of 16 again - even more imprecise.

And now lets solve this mystery:

var_dump(strlen(sprintf('%.0f',  9000000000009051)));

Guess what the result is:

int(16)

Wow. That means floating point numbers basically work precisly down to 16 digits in PHP (if you exceed this magical numbers it is getting imprecise).

So what has happend to your calculation?

It was rounded to the next 16 at the end as you exceeded the number of 16 digits by one.

More information on floating point precision in PHP can be found in dozens of questions on Stackoverflow and of course in the PHP manual.

To answer how to make your code work correctly:

If you want your numbers to be precise do not convert to float. With integers for instance higher calculations should be possible.

If you exceed the number of 16 digits also simple arithmetic like -1 or +1 will fail (at least in the current version of PHP as it seems).

T.Todua
  • 53,146
  • 19
  • 236
  • 237
Blackbam
  • 17,496
  • 26
  • 97
  • 150
  • 1
    wouh, nice answer, thanks! at last a considerable user appeared, who could kindly understand the problem of another programmer. (Btw, to more surprisingly, the downvotes were much more, but seems recently others have upvoted a bit) – T.Todua Jul 12 '18 at 17:03
  • Y I upvoted, too. I simply do not see why this is a bad question just because there have already been different questions about floating point imprecision in PHP (and in this case it would have been a duplicate). I recently talked to another heavy SO user and he is convinced that especially the PHP community on SO is awkward, Python or Ruby questions for instance are treated much friendlier. – Blackbam Jul 12 '18 at 17:12
  • 1
    Well, i have to agree, that somethings strange happens. among my last 5-10 questions, I just get downvotes on very legitimate questions. However, many other respectable SO users already call those users as a "downvote brigade" :) btw, very complete answer you have. I think this should be even a reference from other topics. – T.Todua Jul 12 '18 at 21:36
1

You can't.

The loss of precision when casting an integer to a float is caused by the way floats are stored in the FPU. This is basic computer science knowlegde and not an issue of PHP as a language. For more details on the subject please read https://en.wikipedia.org/wiki/IEEE_754.

However, the calculation itself works just fine for at least PHP 4.3+, according to 3v4l.org. The value you've used is an integer and well below PHP_INT_MAX. You can avoid type casting like this:

print $example . PHP_EOL;
print sprintf('%d',  $example);

You can simlate the error of sprintf() by casting $example to float explicitly:

print (int) (float) $example;  // incorrect
Code4R7
  • 2,600
  • 1
  • 19
  • 42
  • thanks for reply. this was old question, but i still have same stance. for me it's joke that it's 2022yr (we landed mars) and in programming languages we can't `90000000000009132 - 1`. i have never (and will) accept any justifications, let blame it hardware or whatever. the programming language should entail some layer , seeing ('this is beyond max-int or whatever hell, let's use another approach to return user the correct answer for X plus Y'). Yeah, I still think the good programming language could have inbuild solution. anyway, thanks for answer. – T.Todua Feb 19 '22 at 10:56
  • The problem with your view is the way you've asked the question. When I would use a hammer on screws instead of a screwdriver, I can expect trouble. In the same way, when you'd let go of the restriction to store the number with a `float` datatype, the math will work just fine in any programming language with an `u64` or `i32`. And when you'd allow yourself to store the numbers as a `string`, you can utilise the [BC Math functions](https://www.php.net/manual/en/ref.bc.php) in PHP to do math with astronomical precision. – Code4R7 Feb 19 '22 at 13:40
  • Not quite matching example. in php (back in the days at least) most of programs were build using simple variables (there were not used `float, int` or whatever in applications, but pure `$variable`s. so, the example for taking hammer for screws is not on spot. i took the tool that language gave, it was not C# , JAVA or any advanced prog.lang to just easily use type. But as you don't get my point, i wouldnt go further, as I would have said more inherent glitches of php, but you would have argued more. – T.Todua Feb 19 '22 at 14:16
  • 1
    Well, I actually can follow you. The PHP language is limited indeed, in a sense that it hides the implementation specifics. At least with implicit typecasting. And the worst part of it, is that PHP returns the wrong mathematical result without error or warning to the developer. This is one of the reasons I'm happy to switch to Rust instead. – Code4R7 Feb 19 '22 at 15:14
  • thanks for understanding. (*about Rust - well, i am not familiar tbh, but might be interesting lang). – T.Todua Feb 19 '22 at 22:09
  • PHP suffers from [leaky abstractions](https://www.joelonsoftware.com/2002/11/11/the-law-of-leaky-abstractions/). It's not that Rust will handle the FPU differently, but it will warn you for sure, like it will for integer overflows. When you're interested towards Rust, you'll might want to read about [Rust in the Linux kernel](https://thenewstack.io/rust-in-the-linux-kernel-good-enough/) or skim through the [Rust book](https://www.rust-lang.org/learn). – Code4R7 Feb 21 '22 at 08:28