2

There's another thread similar to this, but it is not a duplicate of this, so please don't mark it as such.

I have a communications simulation that takes test data - floats and ints - and converts them to Binary strings, then sends them across a pipe and they're picked-up by a listener at the other end. They're then 'unpacked' and converted back to numbers.

However, the conversion back using Integer.parseInt() throws an Exception, even though the Binary in the message is patently correctly formed IEEE-754 binary32 single-precision format.

    float flt = -45732.9287f;
    int intFloat = Float.floatToRawIntBits(flt);
    String str = Integer.toBinaryString(intFloat);

    System.out.println("------- Now do the Reverse -------");

    int revint = Integer.parseInt(str, 2);

    .... Throws java.lang.NumberFormatException: For input string: "11000111001100101010010011101110"
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)

Using parseInt() without a Radix throws a similar exception:

    "int revint = Integer.parseInt(str);" throws an exception too, "java.lang.NumberFormatException: For input string: "11000111001100101010010011101110"
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)".

So, it isn't merely a Radix usage issue.

Converting back using Long and BigInt would be incorrect because they are 64-bit IEEE-754 formats, meant for Signed Double, not signed Single Precision data types like Float.

I have a solution that fudges the asymetry in Java's conversion methods, but it strikes me as incongruous that there isn't a native API:

    System.out.println("------- Now do the Reverse -------");

    int revSign = str.substring(0,1).equals("1") ? -1: 1;
    System.out.println("The passed-in sign is:"+revSign);

    int revint = Integer.valueOf(str.substring(1), 2);

    float revFloat = Float.intBitsToFloat(revint)*revSign;

I must be missing something.

CelticPoet
  • 588
  • 1
  • 5
  • 13

3 Answers3

3

"11000111001100101010010011101110" is a 32 bits binary number starting with 1. This means it can represent a negative int in 2's complement.

However, Integer.parseInt doesn't expect its input to be in 2's complement representation. It expects the sign to be represented with a leading -, not with a sign bit, as you can read in the Javadoc:

int java.lang.Integer.parseInt(String s, int radix) throws NumberFormatException

Parses the string argument as a signed integer in the radix specified by the second argument. The characters in the string must all be digits of the specified radix (as determined by whether java.lang.Character.digit(char, int) returns a nonnegative value), except that the first character may be an ASCII minus sign '-' ('\u005Cu002D') to indicate a negative value or an ASCII plus sign '+' ('\u005Cu002B') to indicate a positive value. The resulting integer value is returned.

Therefore it treats "11000111001100101010010011101110" as a 32 bits positive number, which is out of range for the int type.

The following can be used to recompute the value of intFloat:

// replace the leading 1 with - if it's a sign bit
str = (str.length () == 32 && str.charAt (0) == '1') ? ('-' + str.substring (1)) : str;
int revint = Integer.parseInt(str, 2);
// for negative number, we must compute the 2's complement of the number returned
// by parseInt
if (revint < 0) {
  revint = -1 * (revint + Integer.MAX_VALUE + 1);
}

Now both intFloat and revint will be equal to -952982290.

EDIT:

Java does have the API you need, you just didn't use the right method. Since toBinaryString(int i) returns "a string representation of the integer argument as an unsigned integer in base 2", you should use Integer.parseUnsignedInt(String, int) in order to recover the value passed to toBinaryString:

float flt = -45732.9287f;
int intFloat = Float.floatToRawIntBits(flt);
String str = Integer.toBinaryString(intFloat);
System.out.println("------- Now do the Reverse -------");
System.out.println (str);
int revint = Integer.parseUnsignedInt(str, 2);
Eran
  • 387,369
  • 54
  • 702
  • 768
  • I already have a solution that does it that way - but the point of the question is, why can I get an IEEE-754 32-bit Binary String from Java, but Java has no way of parsing it back to a Float directly? It seems like an asymetry issue in the code base, and I find it hard to believe that a 'fudge'' is the only way. – CelticPoet Aug 05 '18 at 06:27
  • @CelticPoet this would be an opinion based matter, but I guess they wanted `parseInt` to parse a human readable String, and therefore they don't accept 2's complement representations. Besides, parseInt should work for any radix between 2 and 36. If 2's complement inputs were accepted for radix 2, what inputs would be accepted for negative numbers in any radix > 2? – Eran Aug 05 '18 at 06:33
  • sure, I know this @Eran, which is why I posed the question. The problem is far simpler than that: If Java has an API to generate an IEEE-754 Single Precision Binary String-representation, then I would expect a symetric API for parsing the same format back into a Single Precision Float. The issue with parseInt is a symptom of trying to fudge around it. I have a fudge that solves it, but it seems wrong to me that it would necessary to do it that way. So I assume I must have missed something in the native API suite that someone else will know. – CelticPoet Aug 05 '18 at 06:45
  • @CelticPoet I just read the Javadoc of `toBinaryString` and found out you should use `Integer.parseUnsignedInt(str, 2)` to recover the original value. – Eran Aug 05 '18 at 06:55
  • I read it too @eran, and discounted it. It says, it parses an Unsigned Int, and I'm passing a an IEEE-754 signed-int's Binary - leading bit is a '1' and it's 32-bits long. It also says that "except that the first character may be an ASCII plus sign '+'", and the Binary String's first char is definitely not an ASCII '+'. The parseUnsignedInt() does seem to work, but following the JavaDoc is clearly not an indicator that it would - as too often seems to be the case, you end up blindly tossing-in seemingly irrelevant API's until one works. That's not exactly engineering, that's guessing. – CelticPoet Aug 05 '18 at 15:51
  • @CelticPoet The Javadoc of `toBinaryString` clearly states - `Returns a string representation of the integer argument as an unsigned integer in base 2. The unsigned integer value is the argument plus 2^32 if the argument is negative; otherwise it is equal to the argument. This value is converted to a string of ASCII digits in binary (base 2) with no extra leading 0s. The value of the argument can be recovered from the returned string s by calling Integer.parseUnsignedInt(s, 2).` - hence using `Integer.parseUnsignedInt(s, 2)` is guaranteed to produce the original integer. – Eran Aug 06 '18 at 04:53
1

I must be missing something.

You are missing that the inverse transformation for Integer.toBinaryString(integer) is Integer.parseUnsignedInt(string, 2) ... not Integer.parseInt(string).

As to why they designed the API that way: that would be a question for the designers.

But bear in mind that the Integer API has evolved over a number of years, and a primary requirement for the evolution has been that changes should be backwards compatible. (They can't just rename methods or change semantics because that would break thousands of Java applications written by Oracle's paying customers over the last 20+ years.)

It is also worth noting that parseUnsignedInt was only added in Java 1.8.

Stephen C
  • 698,415
  • 94
  • 811
  • 1,216
  • See my comment to @Eran above. Since I'm trying to give it a Signed Int IEEE-754 Binary representation, neither the name of the method or it's JavaDoc suggest it will do this transformation - and stuffing-in numbers and trying all the API's until one 'appears' to work isn't much of a solution. No-one is suggesting that they make a breaking-change to the language source, but if this transformation is what this API is intended for, I'd at least hope for sensible JavaDoc and a method name that leads you there. – CelticPoet Aug 05 '18 at 15:53
  • If you think the javadocs should be improved, submit a bug report! Or better still, submit a patch with your suggested improvement. – Stephen C Aug 05 '18 at 22:47
0

The answer to my question seems to be that a method with a seemingly opposite name to its functionality, will do the job. A worked example using a Signed Float and "Integer.parseUnsignedInt" is at the bottom.

Unfortunately, "Don't trust the JavaDoc" seems to be a more apt 'answer'. If "Integer.parseUnsignedInt(String, Radix)" is meant for transforming IEEE-754 signed-binary representations of floating point numbers, then it needed a better name and better JavaDoc.

I appreciate everyone participating in the discsussion. For the reader, there are often things the JavaDoc doesn't tell you: Does this method internally chop-off the Sign-bit and parse the rest of the String as an UnsignedInt, then apply the sign-bit afterward? Is that why someone named it "parseUnsignedInt()" despite the fact hat it parses Signed values? Perhaps. What we can say is that, method name notwithstanding, it clearly parses signed Int representations of negative floating point values.

For the next-person who's trying this, the example below will generate an IEEE-754 32-bit Signed Binary String from a Float, conforming to the spec's "binary32" format, and transform it back into the same Signed Binary Float you started with.

    public static void generatingFloatNumbers() {
    float flt = -45732.9287f;
    System.out.println("We start with:"+flt);
    int intFloat = Float.floatToRawIntBits(flt);

    String str = Integer.toBinaryString(intFloat);
    System.out.println("The binary-str for the int is: " + str);
    System.out.println("The binary-str length is: " + str.length() + " chars long.");

    System.out.println("------- Now do the Reverse -------");

    int revint = Integer.parseUnsignedInt(str, 2);
    float revFloat = Float.intBitsToFloat(revint);

    System.out.println("The Reverse Float is: "+revFloat);
}

Output:

We start with:-45732.93
The binary-str for the int is: 11000111001100101010010011101110
The binary-str length is: 32 chars long.
------- Now do the Reverse -------
The Reverse Float is: -45732.93

Process finished with exit code 0

Here's a useful online validation tool so you can visualize the Binary in it's IEEE format and see the conversion.

Note that the same is true is you are starting with Double-to-Long-to-String, and use Long.parseUnsignedLong for the transformation to exchange an IEEE-754 compliant "binary64" format between processes in Binary, except that the output is in Scientific "x.yEn" notation if you try to print it:

    public static void generatingDoubleNumbers2() {
    double dbl = -45732.9287f;
    System.out.println("We start with:"+dbl);
    long longFloat = Double.doubleToRawLongBits(dbl);

    String str = Long.toBinaryString(longFloat);
    System.out.println("The binary-str for the long is: " + str);
    System.out.println("The binary-str length is: " + str.length() + " chars long.");

    System.out.println("------- Now do the Reverse -------");

    double revDbl = Long.parseUnsignedLong(str, 2);

    System.out.println("The Reverse Float is: "+revDbl);
}

The Output:

We start with:-45732.9296875
The binary-str for the long is: 1100000011100110010101001001110111000000000000000000000000000000
The binary-str length is: 64 chars long.
------- Now do the Reverse -------
The Reverse Float is: -4.5468537372761129E18

Process finished with exit code 0
CelticPoet
  • 588
  • 1
  • 5
  • 13