14

I have a hex string that represents a 2's complement number. Is there an easy way (libraries/functions) to translate the hex into a decimal without working directly with its bits??

E.G. This is the expected output given the hex on the left:

"0000" => 0
"7FFF" => 32767 (max positive number)
"8000" => -32768 (max negative number)
"FFFF" => -1

Thanks!

Marsellus Wallace
  • 17,991
  • 25
  • 90
  • 154

3 Answers3

22

This seems to trick java into converting the number without forcing a positive result:

Integer.valueOf("FFFF",16).shortValue(); // evaluates to -1 (short)

Of course this sort of thing only works for 8, 16, 32, and 64-bit 2's complement:

Short.valueOf("FF",16).byteValue(); // -1 (byte)
Integer.valueOf("FFFF",16).shortValue(); // -1 (short)
Long.valueOf("FFFFFFFF",16).intValue(); // -1 (int)
new BigInteger("FFFFFFFFFFFFFFFF",16).longValue(); // -1 (long)

Example here.

trutheality
  • 23,114
  • 6
  • 54
  • 68
  • very interesting! do you know what's actually happening here though? :| – Marsellus Wallace Jul 18 '11 at 14:38
  • I'm pretty sure that converting to a lower-precision type simply keeps the lowest bits, so what was a `0000FFFF` `int` becomes a `FFFF` `short`. Similarly `Integer.valueOf("A7FFFF",16).shortValue();` would also result in `-1` -- the higher bits are just cut off. – trutheality Jul 18 '11 at 19:48
  • You're probably right. Still, I don't fully understand why the sign switches to negative when converting to a lower precision... It probably depends on how numbers are stored internally, I might need to do some research on this.. – Marsellus Wallace Jul 18 '11 at 20:38
  • Actually it's specified in http://java.sun.com/docs/books/jls/second_edition/html/conversions.doc.html#25363 – trutheality Jul 18 '11 at 20:44
  • Particularly "A narrowing conversion of a signed integer to an integral type T simply discards all but the n lowest order bits, where n is the number of bits used to represent type T. In addition to a possible loss of information about the magnitude of the numeric value, this may cause the sign of the resulting value to differ from the sign of the input value." – trutheality Jul 18 '11 at 20:45
  • The [`BigInteger` documentation](http://download.oracle.com/javase/6/docs/api/java/math/BigInteger.html#intValue%28%29) also states that it follows the same convention. So it looks like this behavior is guaranteed. – trutheality Jul 18 '11 at 20:49
  • Yes, I'm pretty sure this will work all the times. It still feels a little bit as java magic though since the sign information is usually stored in the highest bit and this would certainly be truncated in the conversion. I guess numbers are probably stored/encoded as 2's complement internally even after a narrowing conversion. Integer FFFFFF gets truncated to FFFF (65536) that is way too big for a short (max positive value: 2^15-1=32767). At this point Java just follows the 2's complement encoding and produces -1. Thanks! – Marsellus Wallace Jul 18 '11 at 21:13
8

Just write a utility method:

 public static Integer twosComp(String str) throws java.lang.Exception {
       Integer num = Integer.valueOf(str, 16);
       return (num > 32767) ? num - 65536 : num;
 }

Tests:

 twosComp("7FFF") -> 32767
 twosComp("8000") -> -32768
 twosComp("FFFF") -> -1
Mark Elliot
  • 75,278
  • 22
  • 140
  • 160
5

This seems to work reasonably well. It can be fooled by passing it non-standard length strings: "FFF" maps to -1. Zero padding will correct the fault.

You are not clear on what type return you want, so I have returned Number, at whatever size is appropriate.

public Number hexToDec(String hex)  {
   if (hex == null) {
      throw new NullPointerException("hexToDec: hex String is null.");
   }

   // You may want to do something different with the empty string.
   if (hex.equals("")) { return Byte.valueOf("0"); }

   // If you want to pad "FFF" to "0FFF" do it here.

   hex = hex.toUpperCase();

   // Check if high bit is set.
   boolean isNegative =
      hex.startsWith("8") || hex.startsWith("9") ||
      hex.startsWith("A") || hex.startsWith("B") ||
      hex.startsWith("C") || hex.startsWith("D") ||
      hex.startsWith("E") || hex.startsWith("F");

   BigInteger temp;

   if (isNegative) {
      // Negative number
      temp = new BigInteger(hex, 16);
      BigInteger subtrahend = BigInteger.ONE.shiftLeft(hex.length() * 4);
      temp = temp.subtract(subtrahend);
   } else {
      // Positive number
      temp = new BigInteger(hex, 16);
   }

   // Cut BigInteger down to size.
   if (hex.length() <= 2) { return (Byte)temp.byteValue(); }
   if (hex.length() <= 4) { return (Short)temp.shortValue(); }
   if (hex.length() <= 8) { return (Integer)temp.intValue(); }
   if (hex.length() <= 16) { return (Long)temp.longValue(); }
   return temp;
}

Sample output:

"33" -> 51
"FB" -> -5
"3333" -> 13107
"FFFC" -> -4
"33333333" -> 53687091
"FFFFFFFD" -> -3
"3333333333333333" -> 3689348814741910323
"FFFFFFFFFFFFFFFE" -> -2
"33333333333333333333" -> 241785163922925834941235
"FFFFFFFFFFFFFFFFFFFF" -> -1
rossum
  • 15,344
  • 1
  • 24
  • 38
  • Using BigInteger to convert works great without the 'isNegative' logic you have. All I had to do was to create the BigInteger like you did, then call the byteValue/shortValue/intValue/longValue as appropriate for my desired width, and I got the same results you did. Except...I think your value for "33333333" should be 858993459. That's what I got through Java, and the Windows calculator. Try `System.out.println(new BigInteger("33333333", 16).intValue()+" -> "+53687091);` – Eric Aug 13 '14 at 20:24