Visual Basic 2010: Recently in one of my projects I have been tasked with reading several data fields in hex. Each field is three characters long. So this is what I have been doing:
'Read the hex value and convert it to decimal
Dim hexVal as string = "38D"
Dim intVal as integer = Convert.ToInt32(hexVal, 16)
'The integer result gets multiplied by a scaling factor
'(this is set by the manufacturer for each field).
Dim fac as single = 200
Dim final as single = intVal * fac
Debug.Print (final)
This has been working great except in this case: hexVal="FFD" and fac=32 .NET gives me intVal=4093 and the final=130976. However the legacy system I am checking against gives a -96.
I'm a bit confused but theorize that it's in the hex notation. The only documentation I have for the raw data states: Characters are coded per ISO Alphabet No 5 using seven bits per character, that is, without the addition of a parity bit. Three such characters will comprise the field of each 32-bit word.
Am I converting this incorrectly?
Addendum: I have looked into the definitions of the fields and have discovered that almost all are expected to be positive (or unsigned). A few can be negative or positive (signed). Looking at the legacy code, for each hex field they compute a unsigned result and a signed result. If the field is expected to be always positive then they use the unsigned result. Now if the field is expected to be negative or positive then they take the unsigned result and if higher than a ceiling then they use the signed result, otherwise they use the unsigned result. Here is what I have so far with the snippets from below:
Dim hexVal As String, res As Single, decCeiling As Single
Dim paddedHex1 As String, intVal1 As Integer = 0, resVal1 As Single = 0
Dim paddedHex2 As String, intVal2 As Integer = 0, resVal2 As Single = 0
Dim IsUnsignedInput As Boolean
hexVal = "FB1" 'Raw hex input (in this case represents temperature in deg C)
res = 0.125 'A resolution factor. Used to scale the decimal to a real-world nummber
decCeiling = 150 'The maximum temperature we can expect is 150 degree Celcius
IsUnsignedInput = False 'Is field unsigned or signed (for temps we can expect negative and positive)
If hexVal.Length > 8 Then
Throw New Exception("Input '" & hexVal & "' exceeds the max length of a raw input. The max is 8 characters.")
EndIf
'This calcualtion assumes an unsigned value (that is, always a positive number)
paddedHex1 = hexVal.ToString.PadLeft(8, CChar("0"))
intVal1 = Convert.ToInt32(paddedHex1, 16)
resVal1 = intVal1 * res
'This assumes a signed value (that is, could be a negative OR positive number.
'Use two's complement to arrive at the result.
paddedHex2 = hexVal.PadLeft(8, CChar("F"))
Dim sb As New StringBuilder(paddedHex2.Length)
For i As Integer = 0 To paddedHex2.Length - 1
Dim hexDigit As Integer = Convert.ToInt32(paddedHex2(i), 16)
sb.Append((15 - hexDigit).ToString("X"))
Next i
Dim inverted As Integer = Convert.ToInt32(sb.ToString, 16)
intVal2 = -(inverted + 1)
resVal2 = intVal2 * res
'Finally, which result do we use as the final decimal? For our example we get
'resVal1 (unsigned)=+502.125
'resVal2 (signed) = -9.875
'Field is signed so is 502.125 > 150? Yes, so use the signed result of -9.875.
If IsUnsignedInput Then
'If unsigned then we always expect a positive value so use straight conversion.
Debug.Print("Result=" & resVal1)
Else
'If signed then we expect a positive OR negative value
If resVal1 > decCeiling Then
'Standard conversion yields a higher number than expected so use two's complement to get number
Debug.Print("Result=" & resVal2)
Else
'Standard conversion yields a number that is in the expected range so use straight conversion to get number
Debug.Print("Result=" & resVal1)
End If
End If
Doing a back-to-back comparison with the legacy system it all matches up, but it is not working with hex too much in the past, and I am a little cautious. I would appreciate any further feedback on this approach.
intVal=4093 is correct.
final=130976 is also correct.
FFD
can also be interpreted as -3 represented in the two's complement (that's how computers store negative values). 32 * -3 = -96.
FFD = 111111111101 (binary)
In the two's complement, when the first bit is a 1
, it means that the number is negative. In order to negate the number, first invert all the bits and then add 1:
FFD inverted = 000000000010
+ 1 = 000000000011 = 3 (decimal).
Since 3 is the negated number, the real number must be -3.
You can do this in the Hex notation as well. If the first hex digit is >= 8, you have a negative number. So negate the number by doing the following replacements:
0 -> F
1 -> E
2 -> D
3 -> C
4 -> B
5 -> A
6 -> 9
7 -> 8
8 -> 7
9 -> 6
A -> 5
B -> 4
C -> 3
D -> 2
E -> 1
F -> 0
(The second column simply lists the hex digits in reverse order.)
So for FFD you get 002 (hex). You can convert this hex number into a decimal number and add 1 and you get 3. Because this represents a negative value, multiply it by -1. 3 * -1 = -3.
Function ConvertHex(ByVal hexVal As String) As Integer
If hexVal(0) >= "8"c Then 'We have a negative value in the 2's complement
Dim sb As New System.Text.StringBuilder(hexVal.Length)
For i As Integer = 0 To hexVal.Length - 1
'Convert one hex digit into an Integer
Dim hexDigit As Integer = Convert.ToInt32(hexVal(i), 16)
'Invert and append it as hex digit
sb.Append((15 - hexDigit).ToString("X"))
Next i
'Get the inverted hex number as Integer again
Dim inverted As Integer = Convert.ToInt32(sb.ToString(), 16)
'Add 1 to the inverted number in order to get the 2's complement
Return -(inverted + 1)
Else
Return Convert.ToInt32(hexVal, 16)
End If
End Function
But if you always have 3 hex digits you can simply do this
Function ConvertHex3(ByVal hexVal As String) As Integer
Dim number = Convert.ToInt32(hexVal, 16)
If hexVal(0) >= "8"c Then
Return number - &H1000
Else
Return number
End If
End Function
That's much smarter!