I have a function which splits up the whole and fractional part of a real number.
When I feed the function 4.9 it returns 0.9 for the fractional portion - which appears all good and well.
But when I try to compare another number against the 0.9 I am getting unexpected results.
When I run this code…
$ratingAvg = 4.9;
$splitDecimal = getSplitDecimal($ratingAvg);
echo "<p>{$splitDecimal['fraction']}</p>";
$type = gettype($splitDecimal['fraction']);
echo "<p>$type</p>";
if ($splitDecimal['fraction'] == 0.9){
echo "<p>TRUE</p>";
}else{
echo "<p>FALSE</p>";
}
exit();
I get this test output on my screen…
0.9
double
FALSE
What is going wrong in this comparison…
if ($splitDecimal['fraction'] == 0.9){
:-/
Sincerely,
Debbie
Let’s see the code for getSplitDecimal()
Here you go…
function getSplitDecimal($input=NULL){
/**
* Returns components of Decimal number.
*
* Written on: 2014-08-14
* Updated on:
*
* @param Double $input
*
* @return array $splitDecimal {Whole part, Fractional part}
*/
if (is_numeric($input)){
// Valid Number.
$wholePart = (int)$input;
$fractionalPart = $input - $wholePart;
}else{
// Not Number.
$wholePart = NULL;
$fractionalPart = NULL;
}
// Set Array.
$splitDecimal['whole'] = $wholePart;
$splitDecimal['fraction'] = $fractionalPart;
return $splitDecimal;
}//End of getSplitDecimal
Debbie
Most likely this is a floating point precision problem, which is a problem inherent to computers. It happens when you use base2 to represent base10 numbers. Something that can be easily represented in base10 is maybe not so easily represented in base2.
For example:
8 - 6.4 == 1.6 // false
Broadly speaking, if you need to test for exact equivalents, try avoiding fractions. Do you math using whole numbers as much as possible.
80 - 64 == 16 // true
Ok, good…
now, I’m really not sure what the question is.
Does this produce the results you are expecting?
function getSplitDecimal($input=NULL){
/**
* Returns components of Decimal number.
*
* Written on: 2014-08-14
* Updated on:
*
* @param Double $input
*
* @return array $splitDecimal {Whole part, Fractional part}
*/
if (is_numeric($input)){
// Valid Number.
$wholePart = (int)$input;
$fractionalPart = '.'.substr($input, 2);
}else{
// Not Number.
$wholePart = NULL;
$fractionalPart = NULL;
}
// Set Array.
$splitDecimal['whole'] = $wholePart;
$splitDecimal['fraction'] = (float)$fractionalPart;
return $splitDecimal;
}//End of getSplitDecimal
If you want to remove the leading 0 from 0.9, then you can just remove (float) from $splitDecimal[‘fraction’] = (float)$fractionalPart;
Please re-read my OP.
The 0.9 returned by my function is not equaling the 0.9 in my conditional.
And the data-type appears to be okay, so why is my conditional failing?
Debbie
DoubleDee:
Please re-read my OP.
The 0.9 returned by my function is not equaling the 0.9 in my conditional.
And the data-type appears to be okay, so why is my conditional failing?
Debbie
Well, that’s kind of what I’m getting at…Jeff answered that pretty accurately already. The code that I gave above is a workaround for the inherent problem, I am just trying to verify that the code is what you want
Jeff_Mott:
Most likely this is a floating point precision problem, which is a problem inherent to computers. It happens when you use base2 to represent base10 numbers. Something that can be easily represented in base10 is maybe not so easily represented in base2.
For example:
8 - 6.4 == 1.6 // false
Broadly speaking, if you need to test for exact equivalents, try avoiding fractions. Do you math using whole numbers as much as possible.
80 - 64 == 16 // true
Jeff, don’t make me cry… I spent ALL DAY writing this function and taking the approach I did, thinking it was a “good” idea…
Let me explain the method to Debbie’s madness for the day…
People can give a Comment a rating of 1 to 5 stars. I then average these votes, and display it next to the Comment along with the appropriate number of Star icons.
The Avg Rating can be any value between 1 to 5, so I needed some help from PHP to figure out how many Stars to display.
So I came up with the following approach…
1.) Split the Avg Rating value into Whole and Fractional parts
2.) Use the Whole portion to run my FOR loop and display all of the Whole Stars
3.) If there is a Fractional portion, then display a Half-Star.
Well, all of that worked great, so I got greedy and decided to take Paul O’s advice and fine-tune my Partial-Stars down to the 1/10.
So then I created a series of IF-THEN-ELSEIF’s to determine what type of Partial Star (e.g. 70%) to display, but I discovered that my Conditionals were NOT properly recognizing the Fractional part from my function!!
I could be wrong, but what I hear you saying is that everything I just spent the day programming will not work because of the impreciseness of Floating-Point Values?!
Help!!!
Debbie
arout77:
Well, that’s kind of what I’m getting at…Jeff answered that pretty accurately already. The code that I gave above is a workaround for the inherent problem, I am just trying to verify that the code is what you want
I didn’t see Jeff’s response, and I thought you were quoting my function - thus why I got frustrated.
Let me start at the top of this thread and make sure I am following everyone.
In the mean time, please see my latest post and my “thought-process” on all of this.
Thanks,
Debbie
Rounding is your friend.
if ([COLOR="#FF0000"]round([/COLOR]$splitDecimal['fraction'][COLOR="#FF0000"], 1)[/COLOR] == 0.9){
arout77
August 14, 2014, 11:48pm
11
PHP docs tells you in the big red box a little about floats’ lack of precision and not to compare them directly for equality.
http://php.net/manual/en/language.types.float.php
Like I said, the code provided above should work around that, and even gives you the option of dropping the leading 0 by converting it to a string. As long as you use == and not ===, you will be fine
arout77
August 14, 2014, 11:54pm
12
Dear lord…how did we not see that one from the get go?? Use your original code and round() the way Jeff showed, guarantee that will fix it.
Funny you suggested that, because right after might last message - and before I went to supper - I tried the same thing and it worked!
So that leads me to some questions…
1.) If there is a floating-point math error, then why is it when I echo this it produces 0.9…
echo "<p>{$splitDecimal['fraction']}</p>";
2.) How can I see exactly what is being stored in $splitDecimal[‘fraction’] ??
3.) Rounding worked with a simple number like 4.3, but what happens if the Avg Rating = 4 2/3 (i.e. 4.6666666) ??
4.) Isn’t there a way to “truncate” in PHP? Maybe that would be better?
Sincerely,
Debbie
round($splitDecimal[‘fraction’], 1 )
The 1 specifies how many digits after the decimal point to return; i.e., it truncates everything after that.
Rounding works as expected…
if you used round(4.6666666, 2 ) it would return 4.67
arout77:
round($splitDecimal[‘fraction’], 1 )
The 1 specifies how many digits after the decimal point to return; i.e., it truncates everything after that.
Rounding works as expected…
if you used round(4.6666666, 2 ) it would return 4.67
But if $avgRating = 4.99 and we use round, then you get 5 which would be wrong in my mind.
I would rather be conservative and round down, but apparently PHP doesn’t offer that.
Would this maybe do what I want…
$avgRating = 4.999999;
$temp = (floor($avgRating * 100))/100
What do you think?
That way I am not accidentally rounding up to the next whole number on edge cases, but I would also make it so my comparison works, right?
Off Topic:
I decided to dig a little deeper to figure out why on my screen 0.9 wasn’t equal to 0.9 and added this code…
echo "<p>" . number_format($splitDecimal['fraction'], 100) . "</p>";
Turns out this is what my function was actually returning…
[b]0.9000000000000003552713678800500929355621337890625000000000000000000000000000000000000000000000000000[/b]
Not sure why when I echoed things normally, PHP didn’t reveal that to me?!
Sincerely,
Debbie
This is interesting…
Test Code:
$ratingAvg = .999;
$splitDecimal = getSplitDecimal($ratingAvg);
echo "<p>" . '$ratingAvg = ' . $ratingAvg . "</p>";
echo "<p>" . 'number_format($splitDecimal[\\'fraction\\'], 100) = ' . number_format($splitDecimal['fraction'], 100) . "</p>";
echo "<p>" . 'round($splitDecimal[\\'fraction\\'], 2) = ' . round($splitDecimal['fraction'], 2) . "</p>";
echo "<p>" . '(floor($splitDecimal[\\'fraction\\'] * 10)/10) = ' . (floor($splitDecimal['fraction'] * 10)/10) . "</p>";
if ((floor($splitDecimal['fraction'] * 10)/10) == 0.9){
echo "<p>TRUE</p>";
}else{
echo "<p>FALSE</p>";
}
Test Output:
$ratingAvg = 0.999
number_format($splitDecimal['fraction'], 100) = 0.9989999999999999991118215802998747676610946655273437500000000000000000000000000000000000000000000000
round($splitDecimal['fraction'], 2) = 1
(floor($splitDecimal['fraction'] * 10)/10) = 0.9
TRUE
Looks like using floor() along with the multiplication/division is the way to go.
Sincerely,
Debbie
round( 4.99999999999999, 1, PHP_ROUND_HALF_DOWN) would return 4.9 … you can skip floor()
When needed, cast your data types and do === comparisons
I had problems with Tandy Basic a long time ago when calculating VAT. The solution was to multiply the amount by 100 and apply 12.5%
Your problem could be solved by multiplying by 10 then return the modulus of 10. Result is a single digit integer.
I missed that there were extra features with ROUND().
Sadly, it doesn’t work as you would expect…
//****************************************************************************
function getSplitDecimal($input=NULL){
if (is_numeric($input)){
// Valid Number.
$wholePart = (int)$input;
$fractionalPart = $input - $wholePart;
// Compensate for Floating-Point rounding errors.
$fractionalPart2 = (floor($fractionalPart * 10))/10;
$fractionalPart3 = round($fractionalPart, 1, PHP_ROUND_HALF_DOWN);
echo "<p>" . $fractionalPart . "</p>";
echo "<p>" . $fractionalPart2 . "</p>";
echo "<p>" . $fractionalPart3 . "</p>";
echo "<p>" . number_format($fractionalPart2, 100) . "</p>";
echo "<p>" . number_format($fractionalPart3, 100) . "</p>";
exit();
}else{
// Not Number.
$wholePart = NULL;
$fractionalPart = NULL;
}
// Set Array.
$splitDecimal['whole'] = $wholePart;
$splitDecimal['fraction'] = $fractionalPart;
return $splitDecimal;
}//End of getSplitDecimal
//****************************************************************************
$ratingAvg = 1.77;
$getSplitDecimal = getSplitDecimal($ratingAvg);
Here is what the output says…
0.77
0.7
0.8
0.6999999999999999555910790149937383830547332763671875000000000000000000000000000000000000000000000000
0.8000000000000000444089209850062616169452667236328125000000000000000000000000000000000000000000000000
Sad to say, but using my FLOOR() approach is more accurate.
I think I have lost all respect for PHP after this little “experiment” over the last 2 days!!
Regardless of the approach used, these deceptive results are PATHETIC!!! :mad:
What in the hell would a person do if they where working in Banking/Finance or the Scientific Community??? (Is this how Wall Street gets rich?!)
I find it hard to believe that you would get the same lame results in languages like C, C++, or Java…
Maybe this is another sign that it is time to ditch PHP for a more “mature” programming language…
Debbie