11

Is there any way to get the binary representation of a floating point number in PHP? Something like Java's Double.doubleToRawLongBits().

Given a positive floating point number, I'd like to get the largest representable floating-point number which is less than that number. In Java, I can do it like this:

double x = Double.longBitsToDouble(Double.doubleToRawLongBits(d) - 1);

But I'm not seeing anything similar in PHP.

Jenni
  • 1,668
  • 4
  • 20
  • 28
  • I'm pretty sure your Java code is buggy. What happens when only the low bit is set? I suspect ignoring the exponent doesn't work... Or when you go from exponent 0 to -1, do you destroy the NaN etc. bits? – derobert Jan 19 '11 at 00:15
  • 1
    @derobert: If you can find a situation where my Java code doesn't work, aside from ±0, ±Inf, or NaN, let me know (with negative values it gives the smallest value larger than the given value). Positive IEEE floating-point numbers, when converted to integer, are sorted. (And the negative values are reverse-sorted). exponents are stored as an unsigned value with an offset, not twos complement, so -1 really is 1 less than 0. If only the low bit is set, you're looking at the equivalent of Double.MIN_VALUE, and subtracting one gives 0, which is the correct answer. – Jenni Jan 19 '11 at 02:00
  • 1
    A very good article on this can be found here: http://www.cygnus-software.com/papers/comparingfloats/comparingfloats.htm – Jenni Jan 19 '11 at 02:01
  • Thanks for the explanation & education, that sounds like it actually does work then. Also, you should put your code in an answer. Nothing wrong with answering your own question. – derobert Jan 19 '11 at 04:13
  • @derobert - OK, I moved my solution into an answer – Jenni Jan 19 '11 at 14:18

4 Answers4

6

As an additional answer not to the whole question but to the title:

If you want to see how your floats looks as a binary:

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;
}

echo floatToBinStr(0.0000000000000000000000000000000000025).PHP_EOL;
echo floatToBinStr(0.25).PHP_EOL;
echo floatToBinStr(0.5).PHP_EOL;
echo floatToBinStr(-0.5).PHP_EOL;

Output:

0011100010001010100101011010010110110111111110000111101000001111
0011111111010000000000000000000000000000000000000000000000000000
0011111111100000000000000000000000000000000000000000000000000000
1011111111100000000000000000000000000000000000000000000000000000
flori
  • 14,339
  • 4
  • 56
  • 63
  • Sorry, why did you do strrev($packed) before splitting it? – tonix Nov 09 '14 at 18:56
  • 1
    @user3019105 Uh, long ago … I think the bytes (not the bits!) are stored in the opposite direction than normally shown in examples. e.g. http://www.binaryconvert.com/result_float.html?decimal=048046050053 – flori Nov 10 '14 at 09:59
  • So if I do `pack("d", 0.25)` if I have 32 bits floats I get will get this binary string: `00000000 00000000 00000001 01111100` instead of this one: `00111110 10000000 00000000 00000000`??? Right? Why does pack() do this? – tonix Nov 10 '14 at 13:24
  • 1
    Maybe that is the inner representation of floats in PHP? (Maybe it is even machine dependent as the docs for pack() state for "d"?) Take care, you are mirroring the whole line of bits in your example, but just the chunks/bytes are mirrored, the bits in the chunks/bytes not. – flori Nov 10 '14 at 15:14
  • So something like this, `01111100 00000001 00000000 00000000` right? – tonix Nov 10 '14 at 19:29
  • No, `00111110 10000000 00000000 00000000` would change without strrev() to `00000000 00000000 10000000 00111110` – flori Nov 10 '14 at 19:57
  • All right I understood, it is like when I use strrev('abc') == 'cba', that is only the chunks are mirrored (each char is 1 byte) – tonix Nov 10 '14 at 21:56
6

Here is a solution I came up with using Peter Bailey's suggestion. It requires 64-bit version of PHP. I don't claim this to be production-quality in any way, but I'm sharing in case anyone wants to build upon it. (In fact I ended up doing something different entirely after I posted the question, but I leave it here as an intellectual exercise.)

// Returns the largest double-precision floating-point number which
// is less than the given value. Only works for positive values.
// Requires integers to be represented internally with 64 bits (on Linux
// this usually means you're on 64-bit OS with PHP built for 64-bit OS).
// Also requires 64-bit double-precision floating point numbers, but I
// think this is the case pretty much everywhere.
// Returns false on error.
function prevDouble($d) {
  $INT32_MASK = 0xffffffff;
  if((0x1deadbeef >> 32) !== 1) {
    echo 'error: only works on 64-bit systems!';
    return false;
  }
  if($d <= 0) {
    return false;
  }
  $beTest = bin2hex(pack('d', 1.0)); //test for big-endian
  if(strlen($beTest) != 16) {
    echo 'error: system does not use 8 bytes for double precision!';
    return false;
  }

  if($beTest == '3ff0000000000000') {
    $isBE = true;
  }
  else if($beTest == '000000000000f03f') {
    $isBE = false;
  }
  else {
    echo 'error: could not determine endian mode!';
    return false;
  }

  $bin = pack('d', $d);

  //convert to 64-bit int
  $int = 0;
  for($i = 0; $i < 8; $i++)
    $int = ($int << 8) | ord($bin[$isBE ? $i : 7 - $i]);

  $int--;
  //convert back to double
  if($isBE)
    $out = unpack('d', pack('N', ($int >> 32) & $INT32_MASK) . pack('N', $int & $INT32_MASK));
  else
    $out = unpack('d', pack('V', $int & $INT32_MASK) . pack('V', ($int >> 32) & $INT32_MASK));

  return $out[1];
}
Community
  • 1
  • 1
Jenni
  • 1,668
  • 4
  • 20
  • 28
2

This isn't a full answer, but the only way I know of to put a float into binary is with pack()

Peter Bailey
  • 105,256
  • 31
  • 182
  • 206
  • looks like this may work, but i'll have to play around with it... i'll post an update if i get it working. – Jenni Jan 18 '11 at 22:49
  • got a solution with your suggestion. It's not portable, and I ended up doing something different entirely, but I at least posted what I got working in case it would benefit anyone else. – Jenni Jan 19 '11 at 01:47
0

java:

long lnBits = Double.doubleToLongBits(longValue);
Byte[] bits = new byte [] {
   (byte) ((value << 56) >>> 56),
   (byte) ((value << 48) >>> 56),
   (byte) ((value << 40) >>> 56),
   (byte) ((value << 32) >>> 56),
   (byte) ((value << 24) >>> 56),
   (byte) ((value << 16) >>> 56),
   (byte) ((value << 8) >>> 56),
   (byte) (value >>> 56)}

php:

$bits = $bitsFromJava;
$str="";
for($i=0;$i<8;i++){
    $str.=chr($bits[$i]);
}
$longValue=unpack('d',$str);

$bitsToJava=array();
for(str_split(pack($longValue)) as $chr){
    $bitsToJava[]=ord($chr);
}
Tito100
  • 1,660
  • 1
  • 12
  • 12