8

I'm trying to convert a 64-bit float to a 64-bit integer (and back) in php. I need to preserve the bytes, so I'm using the pack and unpack functions. The functionality I'm looking for is basically Java's Double.doubleToLongBits() method. http://docs.oracle.com/javase/7/docs/api/java/lang/Double.html#doubleToLongBits(double)

I managed to get this far with some help from the comments on the php docs for pack():

function encode($int) {
        $int = round($int);

        $left = 0xffffffff00000000;
        $right = 0x00000000ffffffff;

        $l = ($int & $left) >>32;
        $r = $int & $right;

        return unpack('d', pack('NN', $l, $r))[1];
}
function decode($float) {
        $set = unpack('N2', pack('d', $float));
        return $set[1] << 32 | $set[2];
}

And this works well, for the most part...

echo decode(encode(10000000000000));

100000000

echo encode(10000000000000);

1.1710299640683E-305

But here's where it gets tricky...

echo decode(1.1710299640683E-305);

-6629571225977708544

I have no idea what's wrong here. Try it for yourself: http://pastebin.com/zWKC97Z7

You'll need 64-bit PHP on linux. This site seems to emulate that setup: http://www.compileonline.com/execute_php_online.php

kryo
  • 708
  • 1
  • 6
  • 23
  • Might I ask what you need this for? – Daniel W. Jun 04 '14 at 10:25
  • I wanted to make use of all 64 bits for my redis zset scores. It appears predis intentionally loses precision. https://github.com/nicolasff/phpredis/issues/61 – kryo Jun 04 '14 at 11:32
  • So the problem is that that value isn't the same as the encode(...) one, even though it appears that way. `echo encode(10000000000000) - 1.1710299640683E-305; // 2.3525429792377E-319` So the question is, do you want the string to be 100% accurate? – Andrew Jun 07 '14 at 20:18
  • Yes. The idea is that you can convert back and forth. – kryo Jun 08 '14 at 16:05
  • Would you be interested in an ugly solution? Basically if you take the result of your encode function and add the difference to the end (for example 1.171029964068323525429792377E-305) it is "equal". However changing the rightmost digits also results in "equality". – Andrew Jun 09 '14 at 15:55
  • I've had some success with `printf` and php.ini's `precision=`... However, I've decided to stay away from type conversions while using php from now on. – kryo Jun 09 '14 at 16:42

3 Answers3

4
$x = encode(10000000000000);
var_dump($x); //float(1.1710299640683E-305)
echo decode($x); //10000000000000

$y = (float) "1.1710299640683E-305";
var_dump($y); //float(1.1710299640683E-305)
echo decode($y); //-6629571225977708544 

$z = ($x == $y);
var_dump($z); //false

http://www.php.net/manual/en/language.types.float.php

... 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. For a "simple" explanation, see the » floating point guide that's also titled "Why don’t my numbers add up?"

FuzzyTree
  • 30,038
  • 3
  • 46
  • 73
  • Your answer does not solve my problem. Note `1.1710299640683E-305 == 1.1710299640683E-305` returns true – kryo Jun 01 '14 at 19:52
  • You're comparing 2 directly defined floats, which will of course be equal. But if you compare a float returned from your decode method to a directly defined float, then these will not be equal for the reasons explained in the link. – FuzzyTree Jun 01 '14 at 19:58
  • If two floats have the same binary representation, they should be equal, regardless of whether or not they were defined in the same way. All I want to do is convert a long to a double without changing the bits... – kryo Jun 01 '14 at 20:10
1

It is working properly, the only problem in this case is in logic of:

echo decode(1.1710299640683E-305);

You can't use "rounded" and "human readable" output of echo function to decode the original value (because you are loosing precision of this double then).

If you will save the return of encode(10000000000000) to the variable and then try to decode it again it will works properly (you can use echo on 10000000000000 without loosing precision).

Please see the example below which you can execute on http://www.compileonline.com/execute_php_online.php as well:

<?php
    function encode($int) {
        $int = round($int);

        $left = 0xffffffff00000000;
        $right = 0x00000000ffffffff;

        $l = ($int & $left) >>32;
        $r = $int & $right;

        return unpack('d', pack('NN', $l, $r))[1];
    }

    function decode($float) {
        $set = unpack('N2', pack('d', $float));
        return $set[1] << 32 | $set[2];
    }

    echo decode(encode(10000000000000)); // untouched
    echo '<br /><br />';

    $encoded = encode(10000000000000);
    echo $encoded; // LOOSING PRECISION! 
    echo ' - "human readable" version of encoded int<br /><br />';

    echo decode($encoded); // STILL WORKS - HAPPY DAYS!
?>
Jacek Sokolowski
  • 587
  • 4
  • 11
  • How can I get a string representation of `encode(10000000000000)` without losing precision? This must be possible in PHP – kryo Jun 03 '14 at 20:18
  • Setting `precision = 200` in `php.ini` does not seem to help. – kryo Jun 03 '14 at 20:31
  • You can try to use `printf('%.200f', $encoded);` but php will truncate it to 53 anyway. You should even get the notification then: "Requested precision of 200 digits was truncated to PHP maximum of 53 digits in(..)". So you will not be able to do it using standard php solutions (there can be some custom libraries though!). – Jacek Sokolowski Jun 03 '14 at 20:57
0

If you have a reliable fixed decimal point, like in my case and the case of currency, you can multiply your float by some power of 10 (ex. 100 for dollars).

function encode($float) {
     return (int) $float * pow(10, 2);
}
function decode($str) {
    return bcdiv($str, pow(10, 2), 2);
}

However, this doesn't work for huge numbers and doesn't officially solve the problem. Seems like it's impossible to convert from an integer to a float string and back without losing the original integer value in php 5.4

kryo
  • 708
  • 1
  • 6
  • 23