Is casting to float destructive? - php

In PHP, I know we shouldn't do math on floats without things like bcmath, but is the mere act of casting a string to float destructive?
Will expressions like (float)'5.111' == '5.111', always be true? Or will the cast itself change that to something like 5.1110000000000199837 as the number is converted?
The main reason is, just as I use (int) to escape integer values going into a database, I would like to use (float) in the same way, without having to rely on quotes and my escape function.

NO, Casting to a float is almost always destructive.
In your example, 5.111 represented in binary is:
101.00011100011010100111111011111001110110110010001011010000111001...
A float would store 23 digits:
101.0001110001101010011
(5.1109981536865234375)
A double would store 52 digits:
101.0001110001101010011111101111100111011011001000101
(5.1109999999999988773424774990417063236236572265625)
In this case, there wouldn't be a difference. However, in larger numbers, it can affect what you display.
For example:
1025.4995
double:
10000000001.011111111101111100111011011001000101101
(1025.499499999999898136593401432037353515625)
float:
10000000001.011111111101
(1025.499267578125)
You can see the precision starts to drop off dramatically after around 8 digits.
The double would round to 1025.4995 whereas the float would be 1025.4993

You shouldn't use (int) to escape integer values. Use a parametrized query and set the type of your input to 'int'. A much better way!
for an example in mysql/php see:
http://us.php.net/manual/en/mysqli.prepare.php

It depends on whether or not the fractional part can be represented exactly in binary (see Fractions in binary). For example, 0.5 has an exact binary representation but 0.1 does not. If the number does not have an exact representation, you are likely to see a different result when printing it again.

Related

Safely round numbers in php using strings to avoid evil float

I use bcmath for my calculations and I want to round some numbers to n decimal places. Now, I know enough to avoid floats, but what I'm wondering is if the following example is safe and/or if there are better ways to do it?
$number = '123.456'; // number to round as string
$roundedNumber = (string) round($number, 2); // round and cast
// calculations using bcmath continue here...
I think it is, I've ran some experiments and so far it always returned expected result but I'd like second opinion as I'm not 100% positive that in some particular case casting string to float and then float back to string will not output undesired result.
Or is there a better way to do this?
EDIT: before you answer:
bc* functions do not round when third parameter is specified, they just trim the output.
number_format does not allow selection of rounding mode, so it's out
EDIT: What do I consider safe?
Given the number as string and rounding mode, will the function always output correct/expected result and not be affected by casting to float?
I guess that what I'm being afraid of is following:
I provide number say 12.345 as string to round function, it gets casted as float and then my number isn't 12.345 anymore, it may be 12.345xxxx because we all know how float can be represented internally. I'm afraid of that affecting the rounding output. I believe there will be no harm when I cast to 12.345 to string, it will always be '12.345', not '12.345....' right?

Additional digits after decimal points

Select Query giving additional digits after decimal point.
The datatype of the column is decimal(4,2).
The value stored is 1.39 but what i get is 1.3899999999999999.
I can perform round,number_format in php but Is there a way to get the exact value without extra digits after decimal ??
Database is MSSQL.
Most probably your database library is converting the DECIMAL(4, 2) datatype to a float behind the scenes. Now 1.39 cannot be expressed exactly as a floating point number, instead it is approximated to 1.3899999999999999023003738329862244427204132080078125 (use an online converter or do a <?php ini_set("precision", 99); var_dump(1.39);).
There are some workarounds, the simplest one is to convert the decimal number to string at the query level:
SELECT CAST(col AS VARCHAR(4)) AS col_as_string FROM ...
The other solution is to use the number_format function which seems to round the values.
Be aware the PHP has no precise data type for numbers with decimal places. When working with such numbers it will always use an approximite data type (floating point numbers).
So comparing 0.1 + 0.2 with 0.3 for instance may very likely result in false. That means: be careful with equality comparisions.
When displaying such values you should never rely on the default presentation, but use number_format instead. E.g. number_format($value, 2).
If you want to avoid floating point issues completely, then don't use numbers with decimal places. That can be a hassle though (e.g. retrieving the integer 139 for 1.39 and keeping in mind that this value means hundredths).
I suspect 1.39 can't be exactly represented as a float.
I don't know PHP, so here is some Java code to demonstrate:
class DoubleFloat {
public static void main(String[] args) {
float f = 1.39f;
double d = 1.39f;
System.out.println("f = " + f);
System.out.println("d = " + d);
}
}
The output:
f = 1.39
d = 1.3899999856948853
Depends what you are trying to do on the PHP side, but you could store the number as an integer, e.g. 1.39 is stored as 139 and convert it somehow on the PHP side.

Floor function not working

Given the following cod:
$number = 1050.55;
var_dump($number - floor($number));
Why does the above code returns the following result?
float(0.54999999999995)
I want a fixed value like 0.55 in this case. Can you help me please?
Floating point operations are not precise and the remainder errors are common.
If you know, what is your desired precission (eg. two digits after the dot), you can use round() function on the result.
In this case this will be:
$number = 1050.55;
var_dump(round($number - floor($number), 2));
For most floats, binary can only approximately represent the correct number. The rule is to perform floor(), ceil() or fmod() last in a series of calculations. At least only do integer math after you use them. If you cast an int to a float, as in your code, then floor() is not going to behave has you expect.
Use printf() when printing floats. Its conversion routines usually do a much better job and give you the answer you expect when truncating floats.
EDIT: Or, to be more exact, printf() works on the decimal character representation of the number when deciding where to truncate so you don't get any weird, unspecified, binary/decimal conversion artifacts.
See this question. While that is about java and you're asking about PHP the math is the same.

Cast a numeric string as float type data

What is the PHP command that does something similar to intval(), but for decimals?
Eg. I have string "33.66" and I want to convert it to decimal value before sending it to MSSQL.
How about floatval()?
$f = floatval("33.66");
You can shave a few nanoseconds off of type conversions by using casting instead of a function call. But this is in the realm of micro-optimization, so don't worry about it unless you do millions of these operations per second.
$f = (float) "33.66";
I also recommend learning how to use sscanf() because sometimes it's the most convenient solution.
list($f) = sscanf("33.66", "%f");
If you mean a float:
$var = floatval("33.66")
Or
$var = (float)"33.66";
If you need the exact precision of a decimal, there is no such type in PHP. There is the Arbitrary Precision Mathematics extension, but it will return strings, so it's only usefull for you when performing calculations.
You could try floatval, but floats are potentially lossy.
You could try running the number through sprintf to get it to a more correct format. The format string %.2f would produce a floating-point-formatted number with two decimal places. Excess places get rounded.
I'm not sure if sprintf will convert the value to a float internally for formatting, so the lossy problem might still exist. That being said, if you're only worrying about two decimal places, you shouldn't need to worry about precision loss.
php is a loosely typed language. It doesn't matter if you have
$x = 33.66;
or
$x = "33.66";
sending it to mssql will be the same regardless.
Are you just wanting to make sure it is formatted properly, or is an actual float?

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