Signed Magnitude Representation Binaries
This is something I wrote a couple of years ago. It uses a Tally to split the value into its individual characters, and then aggregates each value at their relevant POWER to get the final result:
CREATE FUNCTION [dbo].[SignedBinaryToDec] (@Binary varchar(64))
RETURNS table
AS RETURN
WITH N AS(
SELECT N
FROM (VALUES(NULL),(NULL),(NULL),(NULL))N(N)),
Tally AS(
SELECT TOP (LEN(@Binary)-1) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS I
FROM N N1, N N2, N N3)
SELECT SUM(SS.C * POWER(CONVERT(decimal(2,0),2),T.I-1)) *
CASE LEFT(@Binary,1) WHEN 0 THEN 1
WHEN 1 THEN -1
END AS Dec
FROM Tally T
CROSS APPLY (VALUES(SUBSTRING(REVERSE(@Binary),T.I,1)))SS(C)
WHERE @Binary NOT LIKE '%[^0-1]%';
GO
SELECT Dec
FROM dbo.SignedBinaryToDec('01001001');
How this works
The above is a inline-table value function. This means that it returns a dataset, rather than a scalar value, and you refer to it in the FROM. The reason for this is that multi-line scalar functions can perform quite poorly. In SQL Server 2019+ it can inline user defined scalar functions, however, I'm not sure if such a query would be. As such I wouldn't want to take the chance of using it, when an iTVF with set based logic will be performant.
Let's break this into a few steps:
The Tally
The first part of the query you'll see is the 2 Common Table Expressions (CTEs); N and Tally. N literally just contains 4 rows with the value NULL. Why 4? I'll explain later. Why NULL? Well, it can be any arbitrary value, I just use NULL as its values are meaningless.
Next we have the CTE Tally. Firstly you'll note that it references N in the FROM 3 times; this means that N is CROSS JOINed (using the old ANSI-89 syntax, yes) to itself 3 times, resulting in (at most) 64 rows, 4^3 = 64, which is (you'll notice) the length of the parameter, varchar(64). That is why I used 4 NULL values.
In the SELECT I limit the number of rows to be returned to the length of the varchar value -1. -1 because the binary value is signed, so the first digit in the string won't be used to determine the aggregate value (done later), but to denote if the value is positive or negative.
Finally we have ROW_NUMBER which unsurprisingly gives each row an ascending number, starting from 1. (SELECT NULL) is in the ORDER BY here as we (again) need some arbitrary value.
If we were to stop at here, with your value, this would result in a data set containing 7 rows, with the values 1 to 7. You can test this with the below
DECLARE @Binary varchar(64) = '01001001';
WITH N AS(
SELECT N
FROM (VALUES(NULL),(NULL),(NULL),(NULL))N(N)),
Tally AS(
SELECT TOP (LEN(@Binary)-1) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS I
FROM N N1, N N2, N N3)
SELECT I
FROM Tally;
The outer SELECT
We'll go to the FROM again first. Obviously FROM Tally returns the rows from the CTE Tally, which we just discussed. Next we have the CROSS APPLY to a VALUES table construct. This returns each individual character for the varchar on a separate row. Note we REVERSE the values as you the lowest denomimated is on the right in numbers. If you were to return the results for your value from just the FROM you would end up with the following data set:
| I |
C |
| 1 |
1 |
| 2 |
0 |
| 3 |
0 |
| 4 |
1 |
| 5 |
0 |
| 6 |
0 |
| 7 |
1 |
Note
REVERSE was not in my original solution, not sure how I missed that, but as the value was a palindrome we "lucked" out.
The WHERE is there to stop the query trying to process any invalid values. For example, if you were to input '01001a01' or '013240101' the function would not return a result.
Now the SELECT. We'll break this down.
POWER(CONVERT(decimal(2,0),2),T.I-1)
There a few bits here. Firstly we take the int value 2 and convert it specifically to a decimal(2,0). Then we have POWER which "does what it says on the tin"; it takes the first value and puts it to the power of the second.
So, for the first row (in the above data set), that would be POWER(2,1-1) which is 1. Then we have POWER(2,2-1), POWER(2,3-1), POWER(2,4-1), which are 2, 4, and 8 respectively. As you can tell, these are your binary numbers.
SUM(SS.C * POWER(CONVERT(decimal(2,0),2),T.I-1))
Here we taking the expression before and multiplying it by the digit from the varchar and then aggregate those values. For your example, this means you have an expression that resolves into something like:
SUM((1*1) + (0*2) + (0*4) + (1*8) + (0*16) + (0*32) + (1*64)) = SUM(1 + 0 + 0 + 8 + 0 + 0 + 64) = SUM(1 + 8 + 64) = 73
- The
CASE expression
This literally just inspects the left most character of the varchar. If it's 0 it multiplies the value in the previous step by 1 (unchanging the value), and if it's 1 then by -1, making the value negative. This is an implementation of Signed magnitude representation.
If you are using One's Compliment or Two's Complement this will not give the expected value for negative values. (You are, however, clearly not using Negative Base).
Other methods
Unsigned Binary
CREATE OR ALTER FUNCTION [dbo].[UnsignedBinaryToDec] (@Binary varchar(64))
RETURNS table
AS RETURN
WITH N AS(
SELECT N
FROM (VALUES(NULL),(NULL),(NULL),(NULL))N(N)),
Tally AS(
SELECT TOP (LEN(@Binary)) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS I
FROM N N1, N N2, N N3)
SELECT SUM(SS.C * POWER(CONVERT(decimal(2,0),2),T.I-1)) AS Dec
FROM Tally T
CROSS APPLY (VALUES(SUBSTRING(REVERSE(@Binary),T.I,1)))SS(C)
WHERE @Binary NOT LIKE '%[^0-1]%';
GO
This uses all the same logic as before, so doesn't require further details.
One's Complement
CREATE OR ALTER FUNCTION [dbo].[OnesComplementBinaryToDec] (@Binary varchar(64))
RETURNS table
AS RETURN
WITH N AS(
SELECT N
FROM (VALUES(NULL),(NULL),(NULL),(NULL))N(N)),
Tally AS(
SELECT TOP (LEN(@Binary)-1) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS I
FROM N N1, N N2, N N3)
SELECT
SUM(BW.B * POWER(CONVERT(decimal(2,0),2),T.I-1)) *
CASE LEFT(@Binary,1) WHEN 0 THEN 1
WHEN 1 THEN -1
END AS Dec
FROM Tally T
CROSS APPLY (VALUES(TRY_CONVERT(bit,SUBSTRING(REVERSE(@Binary),T.I,1))))SS(C)
CROSS APPLY (VALUES(CASE LEFT(@Binary,1) WHEN 0 THEN SS.C ELSE ~SS.C END))BW(B)
WHERE @Binary NOT LIKE '%[^0-1]%';
GO
Much of the same here. You'll note, however, the additional CROSS APPLY. ~ is a Bitwise NOT. This means that 1 becomes 0 and 0 becomes 1, so when the first digit is a 1 (denoting the value is negative), then the Bitwise NOT is applied.
Two's Complement
CREATE OR ALTER FUNCTION [dbo].[TwosComplementBinaryToDec] (@Binary varchar(64))
RETURNS table
AS RETURN
WITH N AS(
SELECT N
FROM (VALUES(NULL),(NULL),(NULL),(NULL))N(N)),
Tally AS(
SELECT TOP (LEN(@Binary)-1) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS I
FROM N N1, N N2, N N3)
SELECT
SUM(BW.B * POWER(CONVERT(decimal(2,0),2),T.I-1)) *
CASE LEFT(@Binary,1) WHEN 0 THEN 1
WHEN 1 THEN -1
END - LEFT(@Binary,1) AS Dec
FROM Tally T
CROSS APPLY (VALUES(TRY_CONVERT(bit,SUBSTRING(REVERSE(@Binary),T.I,1))))SS(C)
CROSS APPLY (VALUES(CASE LEFT(@Binary,1) WHEN 0 THEN SS.C ELSE ~SS.C END))BW(B)
WHERE @Binary NOT LIKE '%[^0-1]%';
Identical to One's apart from that you subtract the left most digit.
Example results:
SELECT V.Binary AS B,
U.[Dec] AS U,
S.[Dec] AS S,
[1s].[Dec] AS [1s],
[2s].[Dec] AS [2s]
FROM (VALUES('0000'),('0001'),('0010'),('0011'),
('0100'),('0101'),('0110'),('0111'),
('1000'),('1001'),('1010'),('1011'),
('1100'),('1101'),('1110'),('1111'))V(Binary)
CROSS APPLY dbo.UnsignedBinaryToDec(Binary) U
CROSS APPLY dbo.SignedBinaryToDec(Binary) S
CROSS APPLY dbo.OnesComplementBinaryToDec(Binary) [1s]
CROSS APPLY dbo.TwosComplementBinaryToDec(Binary) [2s]
ORDER BY V.Binary;
| Binary |
Unsigned |
Signed |
One's |
Two's |
| 0000 |
0 |
0 |
0 |
0 |
| 0001 |
1 |
1 |
1 |
1 |
| 0010 |
2 |
2 |
2 |
2 |
| 0011 |
3 |
3 |
3 |
3 |
| 0100 |
4 |
4 |
4 |
4 |
| 0101 |
5 |
5 |
5 |
5 |
| 0110 |
6 |
6 |
6 |
6 |
| 0111 |
7 |
7 |
7 |
7 |
| 1000 |
8 |
0* |
-7 |
-8 |
| 1001 |
9 |
-1 |
-6 |
-7 |
| 1010 |
10 |
-2 |
-5 |
-6 |
| 1011 |
11 |
-3 |
-4 |
-5 |
| 1100 |
12 |
-4 |
-3 |
-4 |
| 1101 |
13 |
-5 |
-2 |
-3 |
| 1110 |
14 |
-6 |
-1 |
-2 |
| 1111 |
15 |
-7 |
0* |
-1 |
* Represents -0