0

I know that type conversion may lead small errors due to the way floating numbers are stored in binary. Yet I do have the use case of casting a float into a string, yet now I have the issue that this may remove a char:

$we_all_float_down_here = (float) 3467.60009765625;
$expected_string = '3467.60009765625';
var_dump($expected_string === (string) $we_all_float_down_here);
echo "Last chars gets stripped:\n"
 . (string) $we_all_float_down_here . "\n"
 . $expected_string;

This outputs:

bool(false)
Last chars gets stripped:
3467.6000976562
3467.60009765625

How can I cast the float to a string without losing any digit in the process?

https://onlinephp.io/c/b3af3


Note:

  • ini_set('precision', 100); won't fix that certain float values are stored unprecisly
  • number_format may work for some numbers just as my example, yet it too does not change the fact that other numbers will too be unprecise, or rather that some floats are identical to other different looking floats. (In decimal, .99999... is (mathematical-correctly) identical to 1, even though it looks different. In php, due to binary representation and memory constraints, the floats for 3467.6000976563 and 3467.60009765625318323146 are (mathematically-incorrectly) identical here.)
asked Jan 9, 2025 at 13:22
9
  • May be duplicate: stackoverflow.com/questions/6876666/… Commented Jan 9, 2025 at 13:25
  • 1
    Depending on a cornucopia of factors, that last 5 may not actually be stored as part of the float's value in the first place. In a nutshell, with floats you can basically never depend on the exact value you'll get for any operation. All that's guaranteed is that you'll get a value close enough to your value, that it'll only occupy a specific memory size, and that operations on it will be pretty fast. For anything else, you need to use a different type. — What exactly is your use case? Commented Jan 9, 2025 at 13:30
  • 1
    You know that floating point numbers are of limited precision, roughly 14 to 16 digits, yet you struggle with it. If you write the original number as a string nothing will be lost, is that a possibility for you? There is also something like BCMath, and others. The problem is that we don't know why you're having a problem with this. As deceze says: What's your use case? Commented Jan 9, 2025 at 13:38
  • @deceze I want to be able to show it as is in the frontend (so I assume I have to have always keep it as string without type casting), and a user may send a float which must be kept as is (so I assume again this must be always a string then), and I want to be able to compare certain values. I could work for that use case to both cast them the same way, as long as the same "error" is applied, it is fine. I was wondering about BCMath, yet that may be overkill. Commented Jan 9, 2025 at 14:05
  • 2
    Keeping user input as strings might be a good idea. After all, users basically do input strings. However, as soon as you want to use these strings as pure floats you run into the same problem. Notice how BCMath simply uses strings to avoid this problem. It might not be overkill. Commented Jan 9, 2025 at 14:10

2 Answers 2

0

Don't use floats. Use string / bcmath when precision is a requirement.


If you don't care about precision, you may circumvent the issue via (float) (string) castin:

$floats = [
 3467.60009765625,
 3467.6000976562,
];
var_dump(array_map(fn(float $float) => (float) $float, $floats));
var_dump(array_map(fn(float $float) => (float) (string) $float, $floats));
---
array(2) {
 [0]=>
 float(3467.60009765625)
 [1]=>
 float(3467.6000976562)
}
array(2) {
 [0]=>
 float(3467.6000976562)
 [1]=>
 float(3467.6000976562)
}

My confusion as to why I thought I was able to "get the float as is" stems from the fact that some floats may not be identical but their string representation is. Yet others floats are both identical in their float values and their string representation.

Given this code:

<?php
$float_pairs = [
 [3467.60009765625, 3467.6000976562],
 [3467.600097656253, 3467.60009765625318323146],
];
function floatToBinStr($value) {
 $bin = '';
 $packed = pack('d', $value); // use 'f' for 32 bit
 foreach(str_split(strrev($packed)) as $char) {
 $bin .= str_pad(decbin(ord($char)), 8, 0, STR_PAD_LEFT);
 }
 return $bin;
}
foreach ($float_pairs as $pair) {
 $float_1 = $pair[0];
 $float_2 = $pair[1];
 $float_1_as_string = (string) $float_1;
 $float_2_as_string = (string) $float_2;
 $they_are_identical = $float_1 === $float_2;
 $their_string_is_identical = (string) $float_1 === (string) $float_2;
 $binary_1 = floatToBinStr($float_1);
 $binary_2 = floatToBinStr($float_2);
 echo '<pre>The floats are identical:', var_dump($they_are_identical), '</pre>';
 echo '<pre>Their string are identical', var_dump($their_string_is_identical), '</pre>';
 echo '<pre>', var_dump($float_1 ), '</pre>';
 echo '<pre>', var_dump($float_2), '</pre>';
 echo '<pre>', var_dump((string) $float_1), '</pre>';
 echo '<pre>', var_dump((string) $float_2), '</pre>';
 echo '<pre>', var_dump($binary_1), '</pre>';
 echo '<pre>', var_dump($binary_2), '</pre>';
 echo '<hr />';
}
die();

One gets the output:

The floats are identical: bool(false)
Their string are identical bool(true)
float(3467.60009765625)
float(3467.6000976562)
string(64) "0100000010101011000101110011001101000000000000000000000000000000"
string(64) "0100000010101011000101110011001100111111111111111111111110010010"
string(31) "3467.6000976562 3467.6000976562"

The floats are identical: bool(true)
Their string are identical bool(true)
float(3467.600097656253)
float(3467.600097656253)
string(64) "0100000010101011000101110011001101000000000000000000000000000111"
string(64) "0100000010101011000101110011001101000000000000000000000000000111"
string(31) "3467.6000976563 3467.6000976563"

This leads to the weird behavior that during a debug session one can see the specific correct float value of 3467.60009765625, yet as soon as one casts their value to a string, the 5 "disappears", as it string representation will always just be 3467.6000976562.

xdebug view implying the precision is kept, yet it is lost during the toString conversion

Yet in the second case, both float representation mean exactly the same number, meaning 3467.60009765625318323146 will never be shown as for php, it IS identical to 3467.6000976563.

xdebug view with identical float

See the code in action at: https://onlinephp.io/c/9a245

answered Jan 13, 2025 at 14:47
Sign up to request clarification or add additional context in comments.

3 Comments

Ah, a rather complex explanation of the problem you ran into, but I get it. The solution should be that you should never assume a float has a certain exact value. It can only have an approximate value, a very close approximate value, but not exact. The exact value only exists in theory, not in practice. It's similar to the thickness of a line in geometry: It doesn't have a thickness. No, it's not zero, it's undefined. The circumference of a circle? Nobody knows what it exactly is. We define it as 2.π and that's that. 🙂
@KIKOSoftware I understand that now, yet still it is bonkers to me that during the PHP xdebug session I can see the actual float as its actaul value, yet "lose" the precision during the cast to string; yet gain it back for that very special float when using number_format. I wonder if this behavior is the same in other programming languages or if this is for php only.
Yes, this is the same in all programming languages. Each piece of code has to decide where the last significant digit is. That will mostly be an estimate based of the precision of the floating point numbers, when you don't do anything. However, programming languages always tend to overestimate this, sometimes by a lot, so you, as the programmer, are responsible for reigning it in.
-3

I believe you want to convert float to string instead of type-casting, in this case you may want to check this answer

Convert float to string in PHP

number_format($we_all_float_down_here, 20, '.', '')
// output
// 3467.60009765625000000000
answered Jan 9, 2025 at 15:04

2 Comments

Yes, this happens to work, by chance, but turn the last 5 into a 6, or add another decimal, and you run into the same problem again. See: 3v4l.org/DBOjm
This was actually a helpful answer to figure out how not to solve this issue. Prevented me from going down the wrong path.

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.