When summing a group of numbers sometimes I end up with some low decimals? Why can it happen when the numbers are parsed as strings? I know there is some %"&! about floats
function parse(){
foreach($_SESSION['import_csv_posts']['result']['csv'] as $key => $post){
$amount = $this->parse_amount($post[$this->param['amount']]);
if($this->param['vat_amount']){
$amount += $this->parse_amount($post[$this->param['vat_amount']]);
}
$this->balance += $amount;
echo "$amount\n";
}
echo "\nbalance = ".$this->balance;
}
function parse_amount($amount){
$amount = strval($amount);
if(strstr($amount, '.') && strstr($amount, ',')){
preg_match('/^\-?\d+([\.,]{1})/', $amount, $match);
$amount = str_replace($match[1], '', $amount);
}
return str_replace(',', '.', $amount);
}
result
-87329.00
-257700.00
-11400.00
-9120.00
-47485.00
-15504.00
122800.00
1836.00
1254.00
200.00
360.00
31680.00
361.60
1979.20
1144.00
7520.00
6249.49
balance = -399.00000000003
The "%"&! about floats" is that floats are simply not precise. There's a certain inaccuracy inherent in how infinite numbers are stored in finite space. Therefore, when doing math with floats, you won't get 100% accurate results.
Your choice is to either round, format numbers to two decimal places upon output, or use strings and the BC Math package, which is slower, but accurate.
Floating point arithmetic is done by the computer in binary, while the results are displayed in decimal. There are many numbers that cannot be represented equally precisely in both systems, therefore there is almost always some difference between what you as a human expect the result to be and what the result actually is when seen as bits (this is the reason that you cannot reliably compare floats for equality).
It does not matter that your numbers are produced through parsing strings, as soon as PHP sees an arithmetic operator it internally converts the strings to numbers.
If you do not require absolute precision (which it looks like you do not, as you are simply displaying stuff) then simply use printf with a format string such as %.2f to limit the number of decimal places.
Related
I'm trying to multiply some small numbers in PHP, but bcmul is returning zero because the float value is being turned into scientific notation.
I tried using sprintf('%.32f',$value) on the small float values, but since the number of decimal places is unknown, it gets the wrong rounding, and then it'll cause rounding errors when multiplying.
Also, I can't use strpos('e',$value) to find out if it's scientific notation number, because it doesn't finds it even if I cast it as a string with (string)$value
Here's some example code:
$value = (float)'7.4e-5'; // This number comes from an API like this
$value2 = (float)3.65; // Another number from API
echo bcmul($value,$value2); // 0
By default the bc-functions round to 0 decimals. You can change this behavior by either using bcscale or by by changing the bcmath.scale value in your php.ini.
Okay, I found a way to solve it, so, here's how to multiply very small floating point numbers without needing to set an explicit scale for the numbers:
function getDecimalPlaces($value) {
// first we get how many decimal places the small number has
// this code was gotten on another StackOverflow answer
$current = $value - floor($value);
for ($decimals = 0; ceil($current); $decimals++) {
$current = ($value * pow(10, $decimals + 1)) - floor($value * pow(10, $decimals + 1));
}
return $decimals;
}
function multiplySmallNumbers($value, $smallvalue) {
$decimals = getDecimalPlaces($smallvalue); // Then we get number of decimals on it
$smallvalue = sprintf('%.'.$decimals.'f',$smallvalue ); // Since bcmul uses the float values as strings, we get the number as a string with the correct number of zeroes
return (bcmul($value,$smallvalue));
}
I have to validate if a float number has maximum two digits.
I've tried a lot of methods but all fails in more or lase cases.
Last of them were:
//fails for 2638655.99
private function hasMoreThanTwoDecimals(string $number): bool
{
$number = abs($number);
$intPart = floor($number);
$floatPart = $number - $intPart;
return (strlen($floatPart) > 4);
}
OR
//fails for 36.62
private function hasMoreThanTwoDecimals(string $number): bool
{
return $number * 100 - floor($number * 100) > 0.00001;
}
What other methods do you use?
You can't determine the exact number of decimals with the float datatype, because the internal representation is binary. In binary, fx. 0.1 can not be represented exactly. That's why loops always should have integer increments.
for ($i = -1; $i < 1; $i += 0.1) {
if ($i == 0) {
echo "Zero is here!";
}
}
will never say "Zero is here!" because of binary rounding issues.
Using an Epsilon
You already tried to use an epsilon (a very small value) for thesholding (here a refactored version of your function):
private function hasMoreThanTwoDecimals(string $number): bool
{
$epsilon = 0.00001;
return fmod($number * 100, 1.0) > $epsilon;
}
but fails for some values. In that case, you need to increase your epsilon value.
String Arithmetic
The more precise way is to avoid float and use string representations instead. This is your best option, since - according to your function signature - your numbers are represented as strings already.
private function hasMoreThanTwoDecimals(string $number): bool
{
return bcmod(bcmul($number, '100'), '1.0') != 0;
}
This needs the BCMath module to be included in your PHP. A package supporting BCMath and other solutions is brick/math.
The Cheap Solution
However, if you really just need to probe the number and not are doing calculations, you can get the desired result with pattern matching using preg_match.
private function hasMoreThanTwoDecimals(string $number): bool
{
// Trailing 0 does not add to number of decimals
$number = rtrim($number, '0');
return preg_match('~\.\d\d\d~', $number);
}
You can explode the number using the . delimeter, then you return the length of the second part :
$num = 2638655.99;
echo strlen(explode('.',$num)[1]); // Echo 2
Taking the question literally, if a binary floating point number has a maximum of two decimal digits after the decimal point, the fractional part must be one of .0, .25, .5, or .75.
All other binary floating point numbers really have more decimal digits, although printout formatting may hide them. For example, the closest IEEE 754 64-bit binary number to 2638655.99 is 2638655.99000000022351741790771484375, which has more than two digits after the decimal point.
You could subtract the integer part and then test for the remainder being one of the four possibilities.
Alternatively, the real question may be how to determine whether displaying the number will show no more than two digits after the decimal point. If so, convert to string using the appropriate method, then locate the decimal point and count the digits after it, for example as suggested in this answer.
$res = preg_match("^[+-]?([0]{1}|[1-9]{1}[0-9]*)(\.?[0-9]{1,2})?$", $num) == true;
would be the best solution in my opinion. You can use signs (optional) and enforce that a number starts with only one zero.
Possible:
+0.10
+123.01
-1
123
Not possible:
00.0
0001.0
1.
123.123
Be aware that preg_match returns 0 if no match is found and false if an error occurred (preg_match)
you can use the number_format
number_format($number, 2, '.', '');
For example...
$aa = 10694994.89;
$bb = 10696193.86;
$ab = $aa - $bb;
// result is:-1198.9699999988 not the -1198,97
But in this exampe:
$cc = 0.89;
$dd = 0.86;
$cd = $cc - $dd;
//Result is: 0.03
Why the difference in to examples? Lacks precision?
None of the numbers in your code can be expressed exactly in binary floating point. They have all been rounded somehow. The question is why one of the results has been (seemingly) rounded to two decimal digits and not the other. The answer lies in the difference between the precision and accuracy of floating point numbers and the precision PHP uses to print them.
Floating point numbers are represented by a significand (or mantissa) in the range [1, 2), which is scaled by multiplying it by a power of two. (This is what the "floating" in floating point means). The precision of the number is determined by the number of digits in the significand. The accuracy is determined by how many of those digits are actually correct. See: How are floating point numbers stored in memory? for more details.
When you echo floating point numbers in PHP, they are first converted to string using the precision configuration setting, which defaults to 14. (In Zend/zend_operators.c)
To see what is really going on, you have to print the numbers using a larger precision:
$aa = 10694994.89;
$bb = 10696193.86;
$ab = $aa - $bb;
printf ("\$aa: %.20G\n", $aa);
printf ("\$bb: %.20G\n", $bb);
printf ("\$ab: %.20G\n\n", $ab);
$cc = 0.89;
$dd = 0.86;
$cd = $cc - $dd;
printf ("\$cc: %.20G\n", $cc);
printf ("\$dd: %.20G\n", $dd);
printf ("\$cd: %.20G\n", $cd);
Output:
$aa: 10694994.890000000596
$bb: 10696193.859999999404
$ab: -1198.9699999988079071
$cc: 0.89000000000000001332
$dd: 0.85999999999999998668
$cd: 0.030000000000000026645
The initial numbers have a precision of about 16 to 17 digits. When you subtract $aa-$bb, the first 4 digits cancel each other out. The result, (while still having a precision of about 16 to 17 digits), is now only accurate to about 12 digits. This lower accuracy shows up when the results is printed using a 14-digit precision.
The other subtraction ($cc-$dd) loses only a single digit of accuracy, which isn't noticable when printed with a 14-digit precision.
This should work for you:
(You have to round your result!)
$aa = 10694994.89;
$bb = 10696193.86;
echo $ab = round($aa - $bb, 2);
MySQL data imoprt mongo database.
price float(15,2) in mysql, mongo is not float(15,2).
I want to Determine a var $price have two decimal places.
eg. 100.00 is right, 100 or 100.0 is wrong.
eg.1
$price = 100.00;
$price have two decimal, it's right.
eg.2
$price = 100.0;
$price have not two decimal, it's wrong.
I like to use Regular Expressions to do these things
function validateTwoDecimals($number)
{
if(preg_match('/^[0-9]+\.[0-9]{2}$/', $number))
return true;
else
return false;
}
(Thanks to Fred-ii- for the corrections)
Everybody is dancing around the fact that floating point numbers don't have a number of decimal places in their internal representation. i.e. in float 100 == 100.0 == 100.00 == 100.000 and are all represented by the same number, effectively 100 and is stored that way.
The number of decimal places in this example only has a context when the number is represented as a string. In which case any string function that counts the number of digits trailing the decimal point could be used to check.
number_format($price, $numberOfDecimalDigits) === $price;
or
strrpos($price, '.') === strlen($price) - 1 - $numberOfDecimalDigits;
Trivia: $price should not be called a "float variable". This is a string that happens to represent a float value. 100.00 as a float has zero decimal digits, and 100.00 === 100 as float :
$price = 100.00;
echo $price; // output: 100
$price2 = (float)100;
echo $price === $price2; // ouput: 1
In order for this to work, the number will need to be wrapped in quotes.
With the many scripts I've tested, using $price = 100.00; without quotes did not work, while $price = 100.10; did, so this is as best as it gets.
<?php
$number = '100.00';
echo $number.'<br>';
$count = explode('.',$number);
echo 'The number of digits after the decimal point is: ' . strlen($count[1]);
if(strlen($count[1]) == 2){
echo "<br>";
echo "There is 2 decimal points.";
}
else{
echo "<br>";
echo "There is not 2 decimal points.";
}
After you format the value, you can check with simply splitting the value as string into 2 parts, for example with explode ...
$ex=explode('.',$in,2); if (strlen($ex[1])==2)
{
// true
}
else
{
// false
}
But again, as i've commented already, if you really have floating input, this is just not a reliable way, as floating numbers are without set decimal places, even if they appears so because of the rounding at the float=>string conversion
What you can do, if you really have floating numbers and wish to have xxx.yy format numbers:
1) convert float to string using round($x,2), so it will round to 2 decimal places.
2) explode the number as i've described, and do the following:
while (strlen($ex[1]<2)) {$ex[1].='0';}
$number=implode('.',$ex);
I would use the following function for that:
function isFloatWith2Decimals($number) {
return (bool) preg_match('/^(?:[1-9]{1}\d*|0)\.\d{2}$/', $number);
}
This will also check if you have only one leading 0 so number like 010.23 won't be considered as valid whereas number like 0.23 will.
And if you don't care about leading 0 you could use simpler method:
function isFloatWith2Decimals($number) {
return (bool) preg_match('/^\d+\.\d{2}$/', $number);
}
Of course numbers need to be passed as string - if you pass 100.00 won't be considered as true, whereas '100.00' will
I have the following function that determines if I sale is fully paid for. I don't remember why I did it this way, but it has been working so far and I don't remember why I had to do it this way.
function _payments_cover_total()
{
//get_payments is a list of payment amounts such as:
//10.20, 10.21, or even 10.1010101101 (10 decimals max)
$total_payments = 0;
foreach($this->sale_lib->get_payments() as $payment)
{
$total_payments += $payment['payment_amount'];
}
//to_currency_no_money rounds total to 2 decimal places
if (to_currency_no_money($this->sale_lib->get_total()) - $total_payments ) > 1e-6 ) )
{
return false;
}
return true;
}
I am wondering if there is ever a case where due to a rounding error that this function would return false when it shouldn't.
The main part I have a question about is:
> 1e-6
I think before I had, but it was causing problems in some cases.
> 0
I think you are doing what is mentioned on php floating help page. To quote it directly :
To test floating point values for equality, an upper bound on the
relative error due to rounding is used. This value is known as the
machine epsilon, or unit roundoff, and is the smallest acceptable
difference in calculations.
$a and $b are equal to 5 digits of precision.
<?php
$a = 1.23456789;
$b = 1.23456780;
$epsilon = 0.00001;
if(abs($a-$b) < $epsilon) {
echo "true";
}
?>
So in your case:
(to_currency_no_money($this->sale_lib->get_total()) - $total_payments) > 1e-6
relative error due to rounding should not be great than 1e-6 or 0.000001
if you are not sure about left operand being greater than right 100% time,then you should add abs() e.g for correctness.
$relative_error=to_currency_no_money($this->sale_lib->get_total()) - $total_payments;
if(abs($relative_error) > 1e-6){
return false
}
return true;
$x = (1.333-1.233)-(1.334-1.234);
echo $x;
//result = $x = -2.2204460492503E-16 - close to zero
//but (1.333-1.233)-(1.334-1.234) = 0.1 - 0.1 = 0 (in calculator)
if($x === 0){
echo "|zero";
}
else {
echo "|non zero"; //<== this is result
}
//screen = -2.2204460492503E-16|non zero
//how to get to zero?
if($x > 1e-6){//1e-6 mathematical constant
echo "|non zero";
}
else {
echo "|zero";//this is result
}
//screen -2.2204460492503E-16|non zero|zero
if ($x > 1e-6 )
{
echo " false";
//echo "|non zero";
//return false;
}
else{
echo " true";//<== this resut
//echo "|zero";
//return true;
}
//screen -2.2204460492503E-16|non zero|zero true
printf("%.1f<br />", 1e-1);
printf("%.2f<br />", 1e-2);
printf("%.3f<br />", 1e-3);
printf("%.4f<br />", 1e-4);
printf("%.5f<br />", 1e-5);
printf("%.6f<br />", 1e-6);
printf("%.7f<br />", 1e-7);
printf("%.8f<br />", 1e-8);
printf("%.9f<br />", 1e-9);
printf("%.10f<br />", 1e-10);
printf("%.11f<br />", 1e-11);
printf("%.12f<br />", 1e-12);
printf("%.29f<br />", -2.2204460492503E-16);
//0.1
//0.01
//0.001
//0.0001
//0.00001
//0.000001
//0.0000001
//0.00000001
//0.000000001
//0.0000000001
//0.00000000001
//0.000000000001
//-0.00000000000000022204460492503
I am sorry, but when dealing with currency, you shouldn't really be using PHP floats, as IMSoP stated. The reason is also from PHP float help pages:
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....
So never trust floating number results to the last digit, and do not
compare floating point numbers directly for equality. If higher
precision is necessary, the arbitrary precision math functions and gmp
functions are available.
Note that the help page specifically says you can't trust float results to the last digit, no matter how short (after comma) it is.
So while you do have very short floats (just 2 digits after comma), the float precision (1e-6) doesn't enter into it really, and you can't trust them 100%.
Since it is a question of money, in order to avoid angry customers and lawsuits accusing of penny shaving (https://en.wikipedia.org/wiki/Salami_slicing), the real solutions are:
1) either use PHP BC math, which works with string representation of numbers
http://php.net/manual/en/book.bc.php
2) as IMSoP suggested, use integers and store the amounts in smallest denomination (cents, eurocents or whatever you have) internally.
First solution might be a bit more resource intense: I haven't used BC math myself much, but storing strings and doing arbitrary precision math (which might be a bit of an overkill in this case) are by definition more RAM and CPU intense than working with integers.
It might, however, need less changes in other parts of the code.
The second solution requires changes to represenation, so that wherever user sees the amounts, they are normalized to dollars,cents (or whatever have you).
Note, however, that in this case also you run problems with rounding risks at some point, as when you do:
float shown_amount; // what we show to customer: in dollars,cents
int real_amount; // what we use internally: in cents
shown_amount = cent_amount / 100;
you may reintroduce the rounding problems as you have floats again and possibilities for rounding errors, so tread carefully and be sure to make calculations and roundings only on real_amount, never on shown_amount