Select record between two IP ranges - sql

I have a table which stores a ID, Name, Code, IPLow, IPHigh such as:
1, Lucas, 804645, 192.130.1.1, 192.130.1.254
2, Maria, 222255, 192.168.2.1, 192.168.2.254
3, Julia, 123456, 192.150.3.1, 192.150.3.254
Now, if I have an IP address 192.168.2.50, how can I retrieve the matching record?
Edit
Based on Gordon's answer (which I'm getting compilation errors) this is what I have:
select PersonnelPC.*
from (select PersonnelPC.*,
(
cast(parsename(iplow, 4)*1000000000 as decimal(12, 0)) +
cast(parsename(iplow, 3)*1000000 as decimal(12, 0)) +
cast(parsename(iplow, 2)*1000 as decimal(12, 0)) +
(parsename(iplow, 1))
) as iplow_decimal,
(
cast(parsename(iphigh, 4)*1000000000 as decimal(12, 0)) +
cast(parsename(iphigh, 3)*1000000 as decimal(12, 0)) +
cast(parsename(iphigh, 2)*1000 as decimal(12, 0)) +
(parsename(iphigh, 1))
) as iphigh_decimal
from PersonnelPC
) PersonnelPC
where 192168002050 between iplow_decimal and iphigh_decimal;
but this gives me an error:
Msg 8115, Level 16, State 2, Line 1
Arithmetic overflow error converting expression to data type int.
Any ideas?

Painfully. SQL Server has lousy string manipulation functions. It does, however, offer parsename(). This approach converts the IP address to a large decimal value for the comparison:
select t.*
from (select t.*,
(cast(parsename(iplow, 4)*1000000000.0 as decimal(12, 0)) +
cast(parsename(iplow, 3)*1000000.0 as decimal(12, 0)) +
cast(parsename(iplow, 2)*1000.0 as decimal(12, 0)) +
cast(parsename(iplow, 1) as decimal(12, 0))
) as iplow_decimal,
(cast(parsename(iphigh, 4)*1000000000.0 as decimal(12, 0)) +
cast(parsename(iphigh, 3)*1000000.0 as decimal(12, 0)) +
cast(parsename(iphigh, 2)*1000.0 as decimal(12, 0)) +
cast(parsename(iphigh, 1) as decimal(12, 0))
) as iphigh_decimal
from t
) t
where 192168002050 between iplow_decimal and iphigh_decimal;
I should note that IP addresses are often stored in the database as the 4-byte unsigned integers. This makes comparisons much easier . . . although you need complicated logic (usually wrapped in a function) to convert the values to a readable format.

Try this simple way checking range
DECLARE #IP NVARCHAR(30)='192.168.500.1'
SELECT * FROM
Branches
WHERE
CAST (PARSENAME(#IP,4) AS INT)>=CAST(PARSENAME(IPLow,4) AS INT) AND CAST(PARSENAME(#IP,3) AS INT)>=CAST(PARSENAME(IPLow,3) AS INT) AND CAST(PARSENAME(#IP,2) AS INT)>=CAST(PARSENAME(IPLow,2) AS INT) AND CAST(PARSENAME(#IP,1) AS INT)>=CAST(PARSENAME(IPLow,1) AS INT)
AND
CAST(PARSENAME( #IP,4) AS INT) <= CAST(PARSENAME(IPHigh ,4) AS INT) AND CAST(PARSENAME(#IP ,3) AS INT) <=CAST(PARSENAME(IPHigh ,3) AS INT) AND CAST(PARSENAME(#IP ,2) AS INT) <=CAST(PARSENAME(IPHigh ,2) AS INT) AND CAST(PARSENAME(#IP ,1) AS INT)<=CAST(PARSENAME(IPHigh ,1) AS INT)
AS Per #Ed Haper Comment Cast is needed.

With this function you can transform any IP address to a form where each part has 3 digits. With this you could do a normal alphanumeric compare. if you want you could return BIGINT too...
CREATE FUNCTION dbo.IPWidth3(#IP VARCHAR(100))
RETURNS VARCHAR(15)
BEGIN
DECLARE #RetVal VARCHAR(15);
WITH Splitted AS
(
SELECT CAST('<x>' + REPLACE(#IP,'.','</x><x>') + '</x>' AS XML) AS IPSplitted
)
SELECT #RetVal = STUFF(
(
SELECT '.' + REPLACE(STR(Part.value('.','int'),3),' ','0')
FROM Splitted.IPSplitted.nodes('/x') AS One(Part)
FOR XML PATH('')
),1,1,'')
FROM Splitted;
RETURN #RetVal;
END
GO
DECLARE #IP VARCHAR(100)='192.43.2.50';
SELECT dbo.IPWidth3(#IP);
The result
192.043.002.050
To reflect Ed Harper's comment here the same function returning a DECIMAL(12,0):
CREATE FUNCTION dbo.IP_as_Number(#IP VARCHAR(100))
RETURNS DECIMAL(12,0)
BEGIN
DECLARE #RetVal DECIMAL(12,0);
WITH Splitted AS
(
SELECT CAST('<x>' + REPLACE(#IP,'.','</x><x>') + '</x>' AS XML) AS IPSplitted
)
SELECT #RetVal =
CAST((
SELECT REPLACE(STR(Part.value('.','int'),3),' ','0')
FROM Splitted.IPSplitted.nodes('/x') AS One(Part)
FOR XML PATH('')
) AS DECIMAL(12,0))
FROM Splitted;
RETURN #RetVal;
END
GO
DECLARE #IP VARCHAR(100)='192.43.2.50';
SELECT dbo.IP_as_Number(#IP);

Use below to fetch the ipLow / IPHigh in 4 columns. You can use those columns to compare Ips.
DECLARE#ip VARCHAR(50)='192.168.0.81'
SELECT (SUBSTRING((#ip), 0,
patindex('%.%',
(#ip))))
,
substring((REPLACE(#ip, (SUBSTRING((#ip), 0,
patindex('%.%',
(#ip)) + 1)),
'')),
0,
patindex('%.%',
((REPLACE(#ip, (SUBSTRING((#ip), 0,
patindex('%.%',
(#ip)) + 1)),
''))))),
SUBSTRING((SUBSTRING(#ip, LEN((SUBSTRING((#ip), 0,
patindex('%.%',
(#ip))))) + 2 + LEN(substring((REPLACE(#ip, (SUBSTRING((#ip), 0,
patindex('%.%',
(#ip)) + 1)),
'')),
0,
patindex('%.%',
((REPLACE(#ip, (SUBSTRING((#ip), 0,
patindex('%.%',
(#ip)) + 1)),
'')))))) + 1,
LEN(#IP) - 1 - LEN(reverse(SUBSTRING(reverse(#ip), 0,
patindex('%.%',
reverse(#ip))))))), 0,
PATINDEX('%.%',
(SUBSTRING(#ip, LEN((SUBSTRING((#ip), 0,
patindex('%.%',
(#ip))))) + 2 + LEN(substring((REPLACE(#ip, (SUBSTRING((#ip), 0,
patindex('%.%',
(#ip)) + 1)),
'')),
0,
patindex('%.%',
((REPLACE(#ip, (SUBSTRING((#ip), 0,
patindex('%.%',
(#ip)) + 1)),
'')))))) + 1,
LEN(#IP) - 1 - LEN(reverse(SUBSTRING(reverse(#ip), 0,
patindex('%.%',
reverse(#ip))))))
))),
reverse(SUBSTRING(reverse(#ip), 0,
patindex('%.%',
reverse(#ip))))

Consider something like this example to convert the address into a number.
CREATE FUNCTION dbo.IPAddressAsNumber (#IPAddress AS varchar(15))
RETURNS bigint
BEGIN
RETURN
CONVERT (bigint,
CONVERT(varbinary(1), CONVERT(int, PARSENAME(#IPAddress, 4))) +
CONVERT(varbinary(1), CONVERT(int, PARSENAME(#IPAddress, 3))) +
CONVERT(varbinary(1), CONVERT(int, PARSENAME(#IPAddress, 2))) +
CONVERT(varbinary(1), CONVERT(int, PARSENAME(#IPAddress, 1))) )
END
and with that you could use standard operators like BETWEEN to find rows within the range you have in the table
DECLARE #t table (ID int, Name varchar(50), Code int, IPLow varchar(15), IPHigh varchar(15))
INSERT INTO #t VALUES
(1, 'Lucas', 804645, '192.130.1.1', '192.130.1.254'),
(2, 'Maria', 222255, '192.168.2.1', '192.168.2.254'),
(3, 'Julia', 123456, '192.150.3.1', '192.150.3.254')
SELECT * FROM #t
WHERE dbo.IPAddressAsNumber('192.168.2.50')
BETWEEN dbo.IPAddressAsNumber(IPLow) AND dbo.IPAddressAsNumber(IPHigh)
The scheme essentially uses PARSENAME to isolate each part of the address, converts each part into a SQL binary string, concatenating the strings together to get a single SQL binary string representing the address, and shows the result as a bigint.
In a textual representation of hexadecimal values think of this as smashing the 4 parts together 192(0xC0) + 168(0xA8) + 2(0x02) + 50(0x32) into 0xC0A80232. When you turn that combined string into its binary digits (0s and 1s) you would end up with something that could be thought of as the address in a binary form used by the network stack in address routing and subnet masking tables. When you turn that into a number in the form of an unsigned integer (or in this case a bigint) you get 3232236082.
Interestingly this scheme gives you a "number" that can be used in place of the original address in lots of ways. You can for example ping the number 2130706433 instead of the address 127.0.0.1 -- the name resolver in Windows will convert it similarly to how DNS is used to find the address of a hostname.
For the sake of completeness, here is another function that can be used to convert the number form back into the standard string form
CREATE FUNCTION dbo.IPAddressFromNumber (#IPNumber AS bigint)
RETURNS varchar(15)
BEGIN
RETURN
CONVERT (varchar(15),
CONVERT(varchar(3), CONVERT(int, SUBSTRING(CONVERT(varbinary(4), #IPNumber), 1,1))) + '.' +
CONVERT(varchar(3), CONVERT(int, SUBSTRING(CONVERT(varbinary(4), #IPNumber), 2,1))) + '.' +
CONVERT(varchar(3), CONVERT(int, SUBSTRING(CONVERT(varbinary(4), #IPNumber), 3,1))) + '.' +
CONVERT(varchar(3), CONVERT(int, SUBSTRING(CONVERT(varbinary(4), #IPNumber), 4,1))) )
END

select *
from ip a
join ip_details b
on a.ip_address >= b.ip_start
and a.ip_address <= b.ip_end;
In this, table "a" contains list of IP address and table "b" contains the IP ranges.
Instead of converting the ip address to numeric we can directly compare the string, it will do a byte by byte comparison.
This is working for me(PostgreSQL).

I was thinking along the lines of Gordon's answer, then realized you don't actually need to mess with numbers. If you zero-pad each part of the address, a string comparison works:
DECLARE #search varchar(50) = '192.168.2.50';
WITH DATA AS (
SELECT * FROM ( values
(1, 'Lucas', '192.130.1.1', '192.130.1.254'),
(2, 'Maria', '192.168.2.1', '192.168.2.254'),
(3, 'Julia', '192.150.3.1', '192.150.3.254')
) AS tbl (ID,Name,IPLow,IPHigh)
)
SELECT *
FROM DATA
WHERE REPLACE(STR(PARSENAME( #search, 4 ), 3, 0), ' ', '0')
+ REPLACE(STR(PARSENAME( #search, 3 ), 3, 0), ' ', '0')
+ REPLACE(STR(PARSENAME( #search, 2 ), 3, 0), ' ', '0')
+ REPLACE(STR(PARSENAME( #search, 1 ), 3, 0), ' ', '0')
BETWEEN
REPLACE(STR(PARSENAME( IPLow, 4 ), 3, 0), ' ', '0')
+ REPLACE(STR(PARSENAME( IPLow, 3 ), 3, 0), ' ', '0')
+ REPLACE(STR(PARSENAME( IPLow, 2 ), 3, 0), ' ', '0')
+ REPLACE(STR(PARSENAME( IPLow, 1 ), 3, 0), ' ', '0')
AND
REPLACE(STR(PARSENAME( IPHigh, 4 ), 3, 0), ' ', '0')
+ REPLACE(STR(PARSENAME( IPHigh, 3 ), 3, 0), ' ', '0')
+ REPLACE(STR(PARSENAME( IPHigh, 2 ), 3, 0), ' ', '0')
+ REPLACE(STR(PARSENAME( IPHigh, 1 ), 3, 0), ' ', '0')
You can, of course, put this inside a UDF for simplicity, though watch out for the performance hit on large queries.
CREATE FUNCTION dbo.IP_Comparable(#IP varchar(50))
RETURNS varchar(50)
WITH SCHEMABINDING
BEGIN
RETURN REPLACE(STR(PARSENAME( #IP, 4 ), 3, 0), ' ', '0')
+ REPLACE(STR(PARSENAME( #IP, 3 ), 3, 0), ' ', '0')
+ REPLACE(STR(PARSENAME( #IP, 2 ), 3, 0), ' ', '0')
+ REPLACE(STR(PARSENAME( #IP, 1 ), 3, 0), ' ', '0')
END
GO
DECLARE #search varchar(50) = '192.168.2.50';
WITH DATA AS (
SELECT * FROM ( values
(1, 'Lucas', '192.130.1.1', '192.130.1.254'),
(2, 'Maria', '192.168.2.1', '192.168.2.254'),
(3, 'Julia', '192.150.3.1', '192.150.3.254')
) AS tbl (ID,Name,IPLow,IPHigh)
)
SELECT *
FROM DATA
WHERE dbo.IP_Comparable(#search) BETWEEN dbo.IP_Comparable(IPLow) AND dbo.IP_Comparable(IPHigh)
This will avoid the issue you're having with integer overflows.

Depends on which record you are looking for the high or the low.
select * from table where IPlow like '192.168.2.50' or IPHigh like '192.168.2.50'

Related

How to query only first letters of name and surname in CONTACTS column in SQL Server

I was asked to query only first letters of name and surname from a column in SQL Server. And the rest should be "*" instead of letters
For example: Waldemar Fisar, should be queried like. W******* F****
Updated question:
I am getting this:
John Snow after query becomes J S
Lora White after query becomes L W
But need to get:
-John Snow should become J*** S***
-Jonathan Conan J******* C****
Lastly, both names and surnames are in the same column
SELECT
Personal info, SUBSTRING([Primary Contact], 1, 1) + ' ' +
SUBSTRING([Primary Contact], CHARINDEX(' ', [Primary Contact]) + 1, 1) AS CI
FROM
xx
You can write a function for that task like the example below:
create function hide_name(#text nvarchar(max), #ch nchar(1), #n int)
returns nvarchar(max)
as
begin
return LEFT(#text, #n) + REPLICATE(#ch, LEN(#text) - #n)
end
go
SELECT
dbo.hide_name(yourNameColumn, '*', 1) + ' ' + dbo.hide_name(yourFamilyNameColumn, '*', 1)
FROM yourTableName
Not recommended, but someone might need
declare #Person table (
name nvarchar(max),
surname nvarchar(max)
);
insert into #Person values ('John', 'Snow'), ('Lora', 'White');
select CONCAT(
IIF(len(name) > 0, concat(LEFT(name, 1), REPLICATE('*', len(name) - 1)), ''),
IIF(len(name) > 0 and len(surname) > 0, ' ', ''),
IIF(len(surname) > 0, concat(LEFT(surname, 1), REPLICATE('*', len(name) - 1)), '')
) as HiddenName
from #Person
SELECT
Personal info, SUBSTRING([Primary Contact], 1, 1) + ' ' +
SUBSTRING([Primary Contact], CHARINDEX(' ', [Primary Contact]) + 1, 1) AS CI
, SUBSTRING([Primary Contact], 1, 1) + replicate('*',CHARINDEX(' ', [Primary Contact])-2)
+ ' ' +
SUBSTRING([Primary Contact], CHARINDEX(' ', [Primary Contact]) + 1, 1)
+ replicate('*',len([Primary Contact]) - CHARINDEX(' ', [Primary Contact])-1) AS CI_Star
FROM
xx
A pure positional solution
DROP TABLE IF EXISTS #names
GO
CREATE TABLE #names(thename NVARCHAR(50))
INSERT INTO #names(thename)
VALUES
('Alison Arnold'),
('Dorothy Jones'),
('Christopher Mackay'),
('Jason H Paterson'),
('Thomas Johnson'),
('Dave')
SELECT subnames.thename,STRING_AGG(subnames.maskedsubname,' ')
FROM
(
SELECT
n.TheName,
SubNames.SubName,
LEFT(SubNames.SubName,1)+REPLICATE('*',(LEN(SubNames.SubName)-1))AS MaskedSubName
FROM #names n
CROSS APPLY(SELECT Value AS SubName FROM STRING_SPLIT(n.TheName,' ')) SubNames
)subnames
GROUP BY SubNames.SubName

Pad Zero before first hypen and remove spaces and add BA and IN

I have data as below
98-45.3A-22
104-44.0A-23
00983-29.1-22
01757-42.5A-22
04968-37.3A2-23
Output Looking for output as below in SQL Server
00098-BA45.3A-IN-22
00104-BA44.0A-IN-23
00983-BA29.1-IN-22
01757-BA42.5A-IN-22
04968-BA37.3A2-IN-23
I splitted parts to cope with tricky data templates. This should work even with non-dash-2-digit tail:
WITH Src AS
(
SELECT * FROM (VALUES
('98-45.3A-22'),
('104-44.0A-23'),
('00983-29.1-22'),
('01757-42.5A-22'),
('04968-37.3A2-23')
) T(X)
), Parts AS
(
SELECT *,
RIGHT('00000'+SUBSTRING(X, 1, CHARINDEX('-',X, 1)-1),5) Front,
'BA'+SUBSTRING(X, CHARINDEX('-',X, 1)+1, 2) BA,
SUBSTRING(X, PATINDEX('%.%',X), LEN(X)-CHARINDEX('-', REVERSE(X), 1)-PATINDEX('%.%',X)+1) P,
SUBSTRING(X, LEN(X)-CHARINDEX('-', REVERSE(X), 1)+1, LEN(X)) En
FROM Src
)
SELECT Front+'-'+BA+P+'-IN'+En
FROM Parts
It returns:
00098-BA45.3A-IN-22
00104-BA44.0A-IN-23
00983-BA29.1-IN-22
01757-BA42.5A-IN-22
04968-BA37.3A2-IN-23
Try this,
DECLARE #String VARCHAR(100) = '98-45.3A-22'
SELECT ISNULL(REPLICATE('0',6 - CHARINDEX('-',#String)),'') -- Add leading Zeros
+ STUFF(
STUFF(#String,CHARINDEX('-',#String),1,'-BA'), -- Add 'BA'
CHARINDEX('-',#String,CHARINDEX('-',#String)+1)+2, -- 2 additional for the character 'BA'
1,'-IN') -- Add 'IN'
What if I have more than 6 digit number before first hyphen and want to remove the leading zeros to make it 6 digits.
DECLARE #String VARCHAR(100) = '0000098-45.3A-22'
SELECT CASE WHEN CHARINDEX('-',#String) <= 6
THEN ISNULL(REPLICATE('0',6 - CHARINDEX('-',#String)),'') -- Add leading Zeros
+ STUFF(
STUFF( #String,CHARINDEX('-',#String),1,'-BA'), -- Add 'BA'
CHARINDEX('-',#String,CHARINDEX('-',#String)+1)+2, -- 2 additional for the character 'BA'
1,'-IN') -- Add 'IN'
ELSE STUFF(
STUFF(
STUFF(#String,CHARINDEX('-',#String),1,'-BA'), -- Add 'BA'
CHARINDEX('-',#String,CHARINDEX('-',#String)+1)+2, -- 2 additional for the character 'BA'
1,'-IN'), -- Add 'IN'
1, CHARINDEX('-',#String) - 6, '' -- remove extra leading Zeros
)
END
Making assumptions that the format is consistent (e.g. always ends with "-" + 2 characters....)
DECLARE #Data TABLE (Col1 VARCHAR(100))
INSERT #Data ( Col1 )
SELECT Col1
FROM (
VALUES ('98-45.3A-22'), ('104-44.0A-23'),
('00983-29.1-22'), ('01757-42.5A-22'),
('04968-37.3A2-23')
) x (Col1)
SELECT RIGHT('0000' + LEFT(Col1, CHARINDEX('-', Col1) - 1), 5)
+ '-BA' + SUBSTRING(Col1, CHARINDEX('-', Col1) + 1, CHARINDEX('.', Col1) - CHARINDEX('-', Col1))
+ SUBSTRING(Col1, CHARINDEX('.', Col1) + 1, LEN(Col1) - CHARINDEX('.', Col1) - 3)
+ '-IN-' + RIGHT(Col1, 2)
FROM #Data
It's not ideal IMO to do this string manipulation all the time in SQL. You could shift it out to your presentation layer, or store the pre-formatted value in the db to save the cost of this every time.
Use REPLICATE AND CHARINDEX:
Replicate: will repeat given character till reach required count specify in function
CharIndex: Finds the first occurrence of any character
Declare #Data AS VARCHAR(50)='98-45.3A-22'
SELECT REPLICATE('0',6-CHARINDEX('-',#Data)) + #Data
SELECT
SUBSTRING
(
(REPLICATE('0',6-CHARINDEX('-',#Data)) +#Data)
,0
,6
)
+'-'+'BA'+ CAST('<x>' + REPLACE(#Data,'-','</x><x>') + '</x>' AS XML).value('/x[2]','varchar(max)')
+'-'+ 'IN'+ '-' + CAST('<x>' + REPLACE(#Data,'-','</x><x>') + '</x>' AS XML).value('/x[3]','varchar(max)')
In another way by using PARSENAME() you can use this query:
WITH t AS (
SELECT
PARSENAME(REPLACE(REPLACE(s, '.', '###'), '-', '.'), 3) AS p1,
REPLACE(PARSENAME(REPLACE(REPLACE(s, '.', '###'), '-', '.'), 2), '###', '.') AS p2,
PARSENAME(REPLACE(REPLACE(s, '.', '###'), '-', '.'), 1) AS p3
FROM yourTable)
SELECT RIGHT('00000' + p1, 5) + '-BA' + p2 + '-IN-' + p3
FROM t;

Converting CHAR string to nth letter in Alphabet string in SQL

I have to build a process that takes a VARCHAR string (for example 'AHT559') and converts it to a INT only string by converting the Alphabetic chars to INTEGERS based on the nth letter in the alphabet. The above would thus result in: 010820559.
I have done this in SAS before, but I'm relatively new to SQL. What would be the best way to do this in SQL?
Here is what I've done in SAS:
DO _i = 1 TO length( account );
IF (rank( char( account, _i ) ) -64) < 0 THEN agreement_hash = CATS( agreement_hash, char( account, _i ) );
ELSE IF (rank( char( account, _i ) ) -64) < 10 THEN agreement_hash = CATS( agreement_hash, 0, rank( char( account, _i ) )-64 );
ELSE agreement_hash = CATS( agreement_hash, rank( char( account, _i ) )-64 );
END;
If the format of the values is always the same as you state in the comments and you only need to process a single value at a time you can do some simple string manipulation to convert the characters to integers using their ASCII values, and subtracting 64 to get the number of the alphabetic character:
SELECT ASCII('A') -- produces 65
SELECT ASCII('A') - 64 -- produces 1
This is a little long winded and could be done in less lines of code, but it's separated for clarity.
DECLARE #val NVARCHAR(10) = 'AHT559'
-- get first, second and third character numeric values
DECLARE #first INT = ASCII(SUBSTRING(#val, 1, 1)) - 64
DECLARE #second INT = ASCII(SUBSTRING(#val, 2, 1)) - 64
DECLARE #third INT = ASCII(SUBSTRING(#val, 3, 1)) - 64
-- join them together adding a '0' if < 10
SELECT RIGHT('0' + CAST(#first AS VARCHAR(2)), 2)
+ RIGHT('0' + CAST(#second AS VARCHAR(2)), 2)
+ RIGHT('0' + CAST(#third AS VARCHAR(2)), 2)
+ RIGHT(#val, 3)
Tested on 4 million rows:
-- temp table creation - takes approx 100 seconds on my machine
CREATE TABLE #temp (val NVARCHAR(6))
DECLARE #rowno INT = 1
SELECT #rowno = 1
WHILE #rowno <= 4000000
BEGIN
INSERT INTO #temp ( val ) VALUES ( 'AHT559' )
SELECT #rowno = #rowno + 1
END
To run this code against the entire temp table takes < 20 seconds on my machine:
SELECT val AS OrignalValue,
RIGHT('0' + CAST( ASCII(SUBSTRING(val, 1, 1)) - 64 AS VARCHAR(2)), 2)
+ RIGHT('0' + CAST( ASCII(SUBSTRING(val, 2, 1)) - 64 AS VARCHAR(2)), 2)
+ RIGHT('0' + CAST( ASCII(SUBSTRING(val, 3, 1)) - 64 AS VARCHAR(2)), 2)
+ RIGHT(val, 3) AS FormattedValue
FROM #temp
Here is a similar script for sqlserver, any character which is not a capital letter is assumed a digit in this syntax:
DECLARE #x varchar(100) = 'AHT559'
DECLARE #p int = len(#x)
WHILE #p > 0
SELECT #x =
CASE WHEN substring(#x, #p, 1) between 'A' and 'Z'
THEN stuff(#x, #p, 1, right(ascii(substring(#x, #p, 1)) - 64 + 100, 2))
ELSE #x END,
#p -= 1
SELECT #x
Result:
010820559
You could use something like the below, possibly as a scalar function to do this conversion.
DECLARE #i INT
DECLARE #Item NVARCHAR(4000) = 'AHT1234'
DECLARE #ItemTable TABLE
(
Item NCHAR(1)
)
SET #i = 1
--Split the input string into separate characters, store in temp table
WHILE (#i <= LEN(#Item))
BEGIN
INSERT INTO #ItemTable(Item)
VALUES(SUBSTRING(#Item, #i, 1))
SET #i = #i + 1
END
DECLARE #AlphaTable TABLE (
Letter NCHAR(1),
Position NVARCHAR(2)
)
-- Populate this with the whole alphabet obviously. Could be a permanent rather than temp table.
INSERT INTO #AlphaTable
( Letter, Position )
VALUES ( N'A', '01'),
(N'H', '08'),
(N'T', '20')
DECLARE #Output NVARCHAR(50)
-- Convert the output and concatenate it back to a single output.
SELECT #Output = COALESCE(#output, '') + Converted
FROM (
SELECT CASE WHEN ISNUMERIC(Item) = 1
THEN CONVERT(NVARCHAR(1), Item)
ELSE (SELECT Position FROM #AlphaTable WHERE Letter = CONVERT(NCHAR(1), Item))
END AS Converted
FROM #ItemTable
) AS T1
SELECT #Output
GO
Try this.
DECLARE #STR VARCHAR(MAX)= 'AHT559',
#SP INT,
#SP_STR VARCHAR(50),
#OUTPUT VARCHAR(MAX)=''
DECLARE #TEMP_STR VARCHAR(50)
SET #TEMP_STR = #STR
WHILE Patindex('%[A-Z]%', #TEMP_STR) <> 0
BEGIN
SELECT #SP = Patindex('%[A-Z]%', #TEMP_STR)
SELECT #SP_STR = Upper(LEFT(#TEMP_STR, #SP))
SELECT #SP_STR = ( Ascii(#SP_STR) - 65 ) + 1
SELECT #TEMP_STR = Stuff(#TEMP_STR, 1, #SP, '')
SET #OUTPUT += RIGHT('0' + #SP_STR, 2)
END
SELECT #OUTPUT + Substring(#STR, Patindex('%[0-9]%', #STR), Len(#STR))
How about using a CTE to create every combination of the first 3 letters and using that to match to:
SQL Fiddle
MS SQL Server 2008 Schema Setup:
CREATE TABLE Accounts
(
Account VARCHAR(6)
)
INSERT INTO Accounts
VALUES ('AHT559'), ('BXC556'),
('CST345')
Query 1:
;WITH AlphaToNum
AS
(
SELECT *
FROM (VALUES
('A', '01'), ('B', '02'), ('C', '03'), ('D', '04'),
('E', '05'), ('F', '06'), ('G', '07'), ('H', '08'),
('I', '09'), ('J', '10'), ('K', '11'), ('L', '12'),
('M', '13'), ('N', '14'), ('O', '15'), ('P', '16'),
('Q', '17'), ('R', '18'), ('S', '19'), ('T', '20'),
('U', '21'), ('V', '22'), ('W', '23'), ('X', '24'),
('Y', '25'), ('Z', '26')
) X(alpha, num)
),
MappingTable
As
(
SELECT A1.alpha + A2.alpha + A3.alpha as match, A1.num + A2.num + A3.num as val
FROM AlphaToNum A1
CROSS APPLY AlphaToNum A2
CROSS APPLY AlphaToNum A3
)
SELECT A.Account, M.val + SUBSTRING(A.Account,4, 3) As ConvertedAccount
FROM MappingTable M
INNER JOIN Accounts A
ON LEFT(A.Account,3) = M.match
Results:
| Account | ConvertedAccount |
|---------|------------------|
| AHT559 | 010820559 |
| BXC556 | 022403556 |
| CST345 | 031920345 |
This is probably best done using a CLR UDF, but a full answer is too long for this format.
Basically you need to create a UDF (User defined function) that takes a string (nvarchar...) as an input and returns a string as an output. You can do that with C# quite easily, and you need to wrap it with the CLR integration requirements.
You can see here for relevant information.
The code could look something like:
[Microsoft.SqlServer.Server.SqlFunction(
IsDeterministic=true,
IsPrecise=true,
SystemDataAccess=SystemDataAccessKind.None)]
public static SqlString ToNthAlpha(SqlString value)
{
if(value.IsNull)
return value;
char []chars = value.Value.ToCharArray();
StringBuilder res = new StringBuilder();
for(int i = 0; i < chars.Length; i++)
{
if(chars[i] >= 'A' && chars[i] <= 'Z')
res.AppendFormat("{0:00}", chars[i] - 'A');
res.Append(chars[i]);
}
return new SqlString(res.ToString());
}

SQL Query to parse numbers from name

The DBMS in this case is SQL Server 2012.
I need a SQL query that will grab just the numbers from a device name. I've got devices that follow a naming scheme that SHOULD look like this:
XXXnnnnn
or
XXXnnnnn-XX
Where X is a letter and n is a number which should be left padded with 0's where appropriate. However, not all of the names are properly padded in this way.
So, imagine you have a column that looks something like this:
Name
----
XXX01234
XXX222
XXX0390-A2
XXX00965-A1
I need an SQL query that will return results from this example column as follows.
Number
------
01234
00222
00390
00965
Anyone have any thoughts? I've tried things like casting the name first as a float and then as an int, but to be honest, I'm just not skilled enough with SQL yet to find the solution.
Any help is greatly appreciated!
SQL Server does not have great string parsing functions. For your particular example, I think a case statement might be the simplest approach:
select (case when number like '___[0-9][0-9][0-9][0-9][0-9]%'
then substring(number, 4, 5)
when number like '___[0-9][0-9][0-9][0-9]%'
then '0' + substring(number, 4, 4)
when number like '___[0-9][0-9][0-9]%'
then '00' + substring(number, 4)
when number like '___[0-9][0-9]%'
then '000' + substring(number, 4, 2)
when number like '___[0-9][0-9]%'
then '0000' + substring(number, 4, 1)
else '00000'
end) as EmbeddedNumber
This might work :
SELECT RIGHT('00000'
+ SUBSTRING(Col, 1, ISNULL(NULLIF((PATINDEX('%-%', Col)), 0) - 1, LEN(Col))), 5)
FROM (SELECT REPLACE(YourColumn, 'XXX', '') Col
FROM YourTable)t
SQLFIDDLE
This will work even when XXX can be of different len:
DECLARE #t TABLE ( n NVARCHAR(50) )
INSERT INTO #t
VALUES ( 'XXXXXXX01234' ),
( 'XX222' ),
( 'X0390-A2' ),
( 'XXXXXXX00965-A1' )
SELECT REPLICATE('0', 5 - LEN(n)) + n AS n
FROM ( SELECT SUBSTRING(n, PATINDEX('%[0-9]%', n),
CHARINDEX('-', n + '-') - PATINDEX('%[0-9]%', n)) AS n
FROM #t
) t
Output:
n
01234
00222
00390
00965
If the first 3 chars are always needed to be removed, then you can do something like that (will work if the characters will start only after '-' sign):
DECLARE #a AS TABLE ( a VARCHAR(100) );
INSERT INTO #a
VALUES
( 'XXX01234' ),
( 'XXX222' ),
( 'XXX0390-A2' ),
( 'XXX00965-A1' );
SELECT RIGHT('00000' + SUBSTRING(a, 4, CHARINDEX('-',a+'-')-4),5)
FROM #a
-- OUTPUT
01234
00222
00390
00965
Another option (will extract numbers after first 3 characters):
SELECT
RIGHT('00000' + LEFT(REPLACE(a, LEFT(a, 3), ''),
COALESCE(NULLIF(PATINDEX('%[^0-9]%',
REPLACE(a, LEFT(a, 3), '')),
0) - 1,
LEN(REPLACE(a, LEFT(a, 3), '')))), 5)
FROM
#a;
-- OUTPUT
01234
00222
00390
00965

Update SQL Data using mask and rules

I have about 3000 entries in a column in SQL 2012 which are unstructured at the moment ie
1.1.01.10, 1.1.1.11
I want to get the data into a format which includes a leading 0 for all single numbers i.e.
01.01.01.10 and so on.
is there any way of doing this with an update query? I can do this by exporting to excel and manipulating there but I want to avoid this if possible.
Alter function Pad
(
#str varchar(max)
)
returns varchar(max)
as
begin
Declare #nstr varchar(max)
while(PATINDEX('%.%',#str)<>0)
begin
Set #nstr = isnull(#nstr,'')+case when PATINDEX('%.%',#str) = 2 then '0'+substring(#str,PATINDEX('%.%',#str)-1,1) else SUBSTRING(#str,1,PATINDEX('%.%',#str)-1) end+'.'
Set #str = case when PATINDEX('%.%',#str) = 2 then stuff(#str,PATINDEX('%.%',#str)-1,2,'') else stuff(#str,1,PATINDEX('%.%',#str),'') end
end
Set #nstr = isnull(#nstr,'')+case when len(#str) <> 1 then #str when len(#str) = 1 then '0'+#str else '' end
return #nstr
end
update t
set num = [dbo].pad(num)
from table t
If the data have always 4 block it's possible to break them in single unit one at a time.
With F AS (
SELECT data
, rem = substring(data, patindex('%.%', data) + 1, len(data))
, value1 = substring(data, 1, patindex('%.%', data) - 1)
FROM Table1
), S AS (
SELECT data
, rem = substring(rem, patindex('%.%', rem) + 1, len(rem))
, value1
, value2 = substring(rem, 1, patindex('%.%', rem) - 1)
FROM F
), T AS (
SELECT data
, value1
, value2
, value3 = substring(rem, 1, patindex('%.%', rem) - 1)
, value4 = substring(rem, patindex('%.%', rem) + 1, len(rem))
FROM S
)
UPDATE T SET
Data = CONCAT(RIGHT('00' + value1, 2), '.'
, RIGHT('00' + value2, 2), '.'
, RIGHT('00' + value3, 2), '.'
, RIGHT('00' + value4, 2));
SQLFiddle Demo
the query can be made smaller, but losing readability.
If the number of block are unknown and/or can change between rows, the query is more complex and involve a recursive CTE
With Splitter AS (
-- anchor
SELECT data
, rem = substring(data, patindex('%.%', data) + 1, len(data))
, pos = len(data) - len(replace(data, '.', '')) + 1
, value = substring(data, 1, patindex('%.%', data) - 1)
, res = CAST('' as nvarchar(50))
FROM Table1
UNION ALL
-- runner
SELECT data
, rem = substring(rem, patindex('%.%', rem) + 1, len(rem))
, pos = pos - 1
, value = substring(rem, 1, patindex('%.%', rem) - 1)
, res = CAST(res + RIGHT('00' + value, 2) + '.' as nvarchar(50))
FROM Splitter
WHERE patindex('%.%', rem) > 1
UNION ALL
-- stop
SELECT data
, rem = ''
, pos = pos - 1
, value = rem
, res = CAST(res + RIGHT('00' + value, 2)
+ '.' + RIGHT('00' + rem, 2) as nvarchar(50))
FROM Splitter
WHERE patindex('%.%', rem) = 0
AND rem <> ''
)
UPDATE table1 Set
Data = res
FROM table1 t
INNER JOIN Splitter s ON t.Data = s.Data and s.Pos = 1
SQLFiddle demo
The anchor query of the CTE get the first block in value, set pos with the number of block and prepare the result (res).
The runner query works for the following block, but not the last one, searching the nth block and adding blocks to the result.
The stop query get the last block without searching for another dot, that will not find, and complete the constrution of the result. Having set the pos initially to the number of blocks, now it'll be 1.