Converting CHAR string to nth letter in Alphabet string in SQL - 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());
}

Related

How to parse SQL Server String in hour, min and second

I have time stored as PT1H22M59.551S.
There it reads read 1 hour 22 min and 59.551 sec.
What is the most efficient way to split this?
Data could be like this. So no separator is mandatory
PT0S
PT6H4M29.212S
PT0S
PT2M55.126S
PT54M4.12S
PT3H6M5.74S
PT16H27M52.069S
You can also create a function to perform the conversion with basic text manipulation:
CREATE FUNCTION dbo.f_GetTime(#TimeInput VARCHAR(50)) RETURNS TIME
AS BEGIN
SET #TimeInput = REPLACE(REPLACE(#TimeInput, 'PT', ''), 'S', '')
IF CHARINDEX('H', #TimeInput) = 0 -- Add missing H
SET #TimeInput = '0H' + #TimeInput
IF CHARINDEX('M', #TimeInput) = 0 -- Add missing M
SET #TimeInput = REPLACE(#TimeInput, 'H', 'H0M')
RETURN CONVERT(TIME, REPLACE(REPLACE(#TimeInput, 'H', ':'), 'M', ':'))
END
GO
Then you can use it with just scalar queries:
SELECT dbo.f_GetTime('PT1H22M59.551S') AS TimeValue
Or you can use it with tables:
DECLARE #Input TABLE (TimeInput VARCHAR(20))
INSERT #Input VALUES ('PT1H22M59.551S'), ('PT0S'), ('PT6H4M29.212S'), ('PT0S'),
('PT2M55.126S'), ('PT54M4.12S'), ('PT3H6M5.74S'), ('PT16H27M52.069S')
SELECT TimeInput, dbo.f_GetTime(TimeInput) AS TimeValue FROM #Input
This would return:
TimeInput TimeValue
-------------------- ----------------
PT1H22M59.551S 01:22:59.5510000
PT0S 00:00:00.0000000
PT6H4M29.212S 06:04:29.2120000
PT0S 00:00:00.0000000
PT2M55.126S 00:02:55.1260000
PT54M4.12S 00:54:04.1200000
PT3H6M5.74S 03:06:05.7400000
PT16H27M52.069S 16:27:52.0690000
--sql server
declare #data table(
lineid int identity,
a varchar(100),
b varchar(100),
hr varchar(20),
[minute] varchar(20),
[second] varchar(20)
)
insert #data(a) select 'PT0S'
insert #data(a) select 'PT6H4M29.212S'
insert #data(a) select 'PT0S'
insert #data(a) select 'PT2M55.126S'
insert #data(a) select 'PT54M4.12S'
insert #data(a) select 'PT3H6M5.74S'
insert #data(a) select 'PT16H27M52.069S'
update #data
set b = replace(a,'PT','')
update #data
set hr = LEFT(b,charindex('H',b))
update #data
set b = REPLACE(b,hr,'')
update #data
set [minute] = LEFT(b,charindex('M',b))
update #data
set b = REPLACE(b,[minute],'')
update #data
set [second] = LEFT(b,charindex('S',b))
update #data
set b = REPLACE(b,[second],'')
update #data
set [hr] = REPLACE([hr],'H',''),[minute]=REPLACE([minute],'M',''),[second]=REPLACE([second],'S','')
update #data
set hr = isnull(nullif(hr,''),'0'),[minute] = isnull(nullif([minute],''),'0'),[second] = isnull(nullif([second],''),'0')
select *,
[hr] + ' hour'+ case when hr <> '1' then 's ' else ' ' end +
[minute] + ' minute'+ case when [minute] <> '1' then 's ' else ' ' end +
[second] + ' second(s)'
from #data
--sql server
declare #data table(
lineid int identity,
[inboundTime] varchar(100)
)
insert #data([inboundTime]) select 'PT0S'
insert #data([inboundTime]) select 'PT6H4M29.212S'
insert #data([inboundTime]) select 'PT0S'
insert #data([inboundTime]) select 'PT2M55.126S'
insert #data([inboundTime]) select 'PT54M4.12S'
insert #data([inboundTime]) select 'PT3H6M5.74S'
insert #data([inboundTime]) select 'PT16H27M52.069S'
insert #data([inboundTime]) select 'PT52.069S'
update #data
set [inboundTime] = replace([inboundTime],'PT','')
SELECT
INBOUND_H
, INBOUND_M
, INBOUND_S
, INBOUND_H*3600 + INBOUND_M*60 inboundTimeMin
, [inboundTime]
from (
SELECT
IIF ( CHARINDEX('H', [inboundTime]) > 0 , SUBSTRING([inboundTime], 0, CHARINDEX('H', [inboundTime])),0) INBOUND_H
, IIF ( CHARINDEX('M', [inboundTime]) > 0 , SUBSTRING([inboundTime], CHARINDEX('H', [inboundTime])+1,CHARINDEX('M', [inboundTime]) - CHARINDEX('H', [inboundTime])-1 ), 0) INBOUND_M
, SUBSTRING([inboundTime], CHARINDEX('M', [inboundTime])+1,CHARINDEX('S', [inboundTime]) - CHARINDEX('M', [inboundTime])-1 ) INBOUND_S
, [inboundTime]
FROM #data)x
This is a more robust function built off solution:
CREATE FUNCTION dbo.f_GetTime(#TimeInput VARCHAR(50)) RETURNS VARCHAR(50)
AS BEGIN
SET #TimeInput = REPLACE(REPLACE(#TimeInput, 'PT', ''), 'S', '')
IF CHARINDEX('H', REVERSE(#TimeInput)) = 1
SET #TimeInput = #TimeInput + '00:00' -- Add missing S
IF CHARINDEX('M', REVERSE(#TimeInput)) = 1
SET #TimeInput = #TimeInput + '00' -- Add missing S
IF CHARINDEX('H', #TimeInput) = 0 -- Add missing H
SET #TimeInput = '00H' + #TimeInput
IF CHARINDEX('M', #TimeInput) = 0 -- Add missing M
SET #TimeInput = REPLACE(#TimeInput, 'H', 'H0M')
RETURN CONVERT(VARCHAR(50), REPLACE(REPLACE(#TimeInput, 'H', ':'), 'M', ':'))
END
GO

MSSQL - Masking data based on mapping table

wanted to perform data masking according to mapping as below by using MSSQL 2008R2:
Mapping Table
A = C
B = A
C = E
1 = 3
2 = 1
3 = 9
Original
ABC123
Masked
CAE319
The idea would be using replace however the second replace function will replacing previous replaced value.
select Replace(Replace(Replace(Replace(Replace(REPLACE('ABC123', 'A', 'C'), 'B', 'A'), 'C', 'E'), '1', '3'), '2', '1'), '3', '9')
Result: CAE319
P.s. value edited, because Reverse or reverse replace cannot be use in this case
any idea?
If you want a more table approach.
There are two code segments below which will Mask or UnMask a string. Easily converted into a UDF or even placed in a CROSS APPLY
Declare #Mask table (MapFrom varchar(10),MapTo varchar(10))
Insert into #Mask values
('A','C'),
('B','D'),
('C','E'),
('1','2'),
('2','3'),
('3','9')
Declare #Yourtable table (ID int,SomeCol varchar(max))
Insert Into #Yourtable values
(1,'ABC123')
-- To Mask
Declare #U varchar(max) ='ABC123'
Select NewSting = Stuff((Select ''+S
From (
Select N
,S=IsNull(MapTo,Substring(#U,N,1))
From (Select Top (Len(#U)) N=Row_Number() Over (Order By (Select null)) From master..spt_values) N
Left Join #Mask on Substring(#U,N,1)=MapFrom
) X
Order By N
For XML Path ('')),1,0,'')
-- To UnMask
Declare #M varchar(max) = 'CDE239'
Select NewSting = Stuff((Select ''+S
From (
Select N
,S=IsNull(MapFrom,Substring(#M,N,1))
From (Select Top (Len(#M)) N=Row_Number() Over (Order By (Select null)) From master..spt_values) N
Left Join #Mask on Substring(#M,N,1)=MapTo
) X
Order By N
For XML Path ('')),1,0,'')
Just change the order of replace and reverse the result
select REVERSE( Replace(Replace(Replace(Replace(Replace(REPLACE('321CBA', '3', '9'), '2', '3'), '1', '2'), 'C', 'E'), 'B', 'D'), 'A', 'C'))
RESULT :
CDE239
EDIT:
Declare #Mask table (MapFrom varchar(10),MapTo varchar(10))
Insert into #Mask values
('A','C'),
('B','A'),
('C','E'),
('1','3'),
('2','1'),
('3','9')
DECLARE #pos INT
,#result VARCHAR(100)
,#maskfrom NCHAR(1)
,#mask_to NCHAR(1);
SET #result = 'ABC123';
SET #pos = 1
WHILE #pos < LEN(#result) + 1
BEGIN
SELECT #mask_to = MapTo
FROM #mask
WHERE MapFrom = substring(#result, #pos, 1)
SET #result = STUFF(#result, #pos, 1, #mask_to);
SET #pos = #pos + 1;
END
SELECT #result
RESULT
CAE319

SQL Server 2005 Using CHARINDEX() To split a string

How can I split the following string based on the '-' character?
So if I had this string: LD-23DSP-1430
How could I split it into separate columns like this:
LD 23DSP 1430
Also, is there a way to split each character into a separate field if I needed to (without the '-')? I'm trying to find a way to replace each letter with the NATO alphabet.
So this would be..... Lima Delta Twenty Three Delta Sierra Papa Fourteen Thirty.... in one field.
I know I can get the left side like this:
LEFT(#item, CHARINDEX('-', #item) - 1)
I wouldn't exactly say it is easy or obvious, but with just two hyphens, you can reverse the string and it is not too hard:
with t as (select 'LD-23DSP-1430' as val)
select t.*,
LEFT(val, charindex('-', val) - 1),
SUBSTRING(val, charindex('-', val)+1, len(val) - CHARINDEX('-', reverse(val)) - charindex('-', val)),
REVERSE(LEFT(reverse(val), charindex('-', reverse(val)) - 1))
from t;
Beyond that and you might want to use split() instead.
Here's a little function that will do "NATO encoding" for you:
CREATE FUNCTION dbo.NATOEncode (
#String varchar(max)
)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN (
WITH L1 (N) AS (SELECT 1 UNION ALL SELECT 1),
L2 (N) AS (SELECT 1 FROM L1, L1 B),
L3 (N) AS (SELECT 1 FROM L2, L2 B),
L4 (N) AS (SELECT 1 FROM L3, L3 B),
L5 (N) AS (SELECT 1 FROM L4, L4 C),
L6 (N) AS (SELECT 1 FROM L5, L5 C),
Nums (Num) AS (SELECT Row_Number() OVER (ORDER BY (SELECT 1)) FROM L6)
SELECT
NATOString = Substring((
SELECT
Convert(varchar(max), ' ' + D.Word)
FROM
Nums N
INNER JOIN (VALUES
('A', 'Alpha'),
('B', 'Beta'),
('C', 'Charlie'),
('D', 'Delta'),
('E', 'Echo'),
('F', 'Foxtrot'),
('G', 'Golf'),
('H', 'Hotel'),
('I', 'India'),
('J', 'Juliet'),
('K', 'Kilo'),
('L', 'Lima'),
('M', 'Mike'),
('N', 'November'),
('O', 'Oscar'),
('P', 'Papa'),
('Q', 'Quebec'),
('R', 'Romeo'),
('S', 'Sierra'),
('T', 'Tango'),
('U', 'Uniform'),
('V', 'Victor'),
('W', 'Whiskey'),
('X', 'X-Ray'),
('Y', 'Yankee'),
('Z', 'Zulu'),
('0', 'Zero'),
('1', 'One'),
('2', 'Two'),
('3', 'Three'),
('4', 'Four'),
('5', 'Five'),
('6', 'Six'),
('7', 'Seven'),
('8', 'Eight'),
('9', 'Niner')
) D (Digit, Word)
ON Substring(#String, N.Num, 1) = D.Digit
WHERE
N.Num <= Len(#String)
FOR XML PATH(''), TYPE
).value('.[1]', 'varchar(max)'), 2, 2147483647)
);
This function will work on even very long strings, and performs pretty well (I ran it against a 100,000-character string and it returned in 589 ms). Here's an example of how to use it:
SELECT NATOString FROM dbo.NATOEncode('LD-23DSP-1430');
-- Output: Lima Delta Two Three Delta Sierra Papa One Four Three Zero
I intentionally made it a table-valued function so it could be inlined into a query if you run it against many rows at once, just use CROSS APPLY or wrap the above example in parentheses to use it as a value in the SELECT clause (you can put a column name in the function parameter position).
Try the following query:
DECLARE #item VARCHAR(MAX) = 'LD-23DSP-1430'
SELECT
SUBSTRING( #item, 0, CHARINDEX('-', #item)) ,
SUBSTRING(
SUBSTRING( #item, CHARINDEX('-', #item)+1,LEN(#ITEM)) ,
0 ,
CHARINDEX('-', SUBSTRING( #item, CHARINDEX('-', #item)+1,LEN(#ITEM)))
),
REVERSE(SUBSTRING( REVERSE(#ITEM), 0, CHARINDEX('-', REVERSE(#ITEM))))
USE [master]
GO
/****** this function returns Pakistan where as if you want to get ireland simply replace (SELECT SUBSTRING(#NEWSTRING,CHARINDEX('$#$#$',#NEWSTRING)+5,LEN(#NEWSTRING))) with
SELECT #NEWSTRING = (SELECT SUBSTRING(#NEWSTRING, 0,CHARINDEX('$#$#$',#NEWSTRING)))******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE FUNCTION [dbo].[FN_RETURN_AFTER_SPLITER]
(
#SPLITER varchar(max))
RETURNS VARCHAR(max)
AS
BEGIN
--declare #testString varchar(100),
DECLARE #NEWSTRING VARCHAR(max)
-- set #teststring = '#ram?eez(ali)'
SET #NEWSTRING = #SPLITER ;
SELECT #NEWSTRING = (SELECT SUBSTRING(#NEWSTRING,CHARINDEX('$#$#$',#NEWSTRING)+5,LEN(#NEWSTRING)))
return #NEWSTRING
END
--select [dbo].[FN_RETURN_AFTER_SPLITER] ('Ireland$#$#$Pakistan')
Create FUNCTION [dbo].[fnSplitString]
(
#string NVARCHAR(200),
#delimiter CHAR(1)
)
RETURNS #output TABLE(splitdata NVARCHAR(10)
)
BEGIN
DECLARE #start INT, #end INT
SELECT #start = 1, #end = CHARINDEX(#delimiter, #string)
WHILE #start < LEN(#string) + 1 BEGIN
IF #end = 0
SET #end = LEN(#string) + 1
INSERT INTO #output (splitdata)
VALUES(SUBSTRING(#string, #start, #end - #start))
SET #start = #end + 1
SET #end = CHARINDEX(#delimiter, #string, #start)
END
RETURN
END**strong text**
DECLARE #variable VARCHAR(100) = 'LD-23DSP-1430';
WITH Split
AS ( SELECT #variable AS list ,
charone = LEFT(#variable, 1) ,
R = RIGHT(#variable, LEN(#variable) - 1) ,
'A' AS MasterOne
UNION ALL
SELECT Split.list ,
LEFT(Split.R, 1) ,
R = RIGHT(split.R, LEN(Split.R) - 1) ,
'B' AS MasterOne
FROM Split
WHERE LEN(Split.R) > 0
)
SELECT *
FROM Split
OPTION ( MAXRECURSION 10000 );

How to compare SQL strings that hold version numbers like .NET System.Version class? [duplicate]

This question already has answers here:
Closed 10 years ago.
Possible Duplicate:
How to compare software versions using SQL Server?
I am a complete and utterly new to sql, but today I found an error in a sql query here at work that is very important. So I could use some help:
Given an sql string that represents a product version
'15.0.0.0'
is there a foolproof way to sort or compare that string similar to how .NET class System.Version compares instances?
So, given such a hypothetical construct, or function or what-ever, I would expect that '15.5.568' would be greater than '15.0.0.0'.
Thanks
Assuming SQL Server, and a known maximum number of parts, here's a user defined function that does the same as parsename, but works on any number of parts:
Create Function dbo.VersionNthPart(#version as nvarchar(max), #part as int) returns int as
Begin
Declare
#ret as int = null,
#start as int = 1,
#end as int = 0,
#partsFound as int = 0
if #version is not null
Begin
Set #ret = 0
while #partsFound < #part
Begin
Set #end = charindex('.', #version, #start)
If #end = 0
Set #partsFound = #part -- bail early
else
Begin
Set #partsFound = #partsFound + 1
If #partsFound = #part
Set #ret = Convert(int, substring(#version, #start, #end - #start))
Else
Set #start = #end + 1
End
End
End
return #ret
End
Example Usage:
With
tmp
As (
Select '1.0.0.5' As Version
Union All Select '1.5.0.06'
Union All Select '1.0.0.06'
Union All Select '2.0.0.0'
Union All Select '2.0.1.1'
Union All Select '15.5.568'
Union All Select '15.0.0.0'
Union All Select '15.15.1323.22'
Union All Select '15.15.622.55'
)
Select
*
From
tmp
Order By
dbo.VersionNthPart(Version, 1),
dbo.VersionNthPart(Version, 2),
dbo.VersionNthPart(Version, 3),
dbo.VersionNthPart(Version, 4)
http://sqlfiddle.com/#!3/e942b/3
Just adding to what #Gordon suggested, here's an example with ParseName
; WITH tmp
AS
(
SELECT '1.0.0.5' AS Version
UNION ALL SELECT '1.5.0.06'
UNION ALL SELECT '1.0.0.06'
UNION ALL SELECT '2.0.0.0'
UNION ALL SELECT '2.0.1.1'
UNION ALL SELECT '15.15.1323.22'
UNION ALL SELECT '15.15.622.55'
)
SELECT *
FROM
(
SELECT CAST(PARSENAME(Version, 4) AS INT) AS col1
, CAST(PARSENAME(Version, 3) AS INT) AS col2
, CAST(PARSENAME(Version, 2) AS INT) AS col3
, CAST(PARSENAME(Version, 1) AS INT) AS col4
FROM tmp
) t0
ORDER BY col1, col2, col3, col4
If you have no more than three decimal points, then you can use parsename. The following rectifies the two version numbers to values having 4 characters, so a string comparison owrks. You example would be "0015.0000.0000.0000.0000".
select (case when (right('0000'+coalesce(parsename(v1, 4), '', 4)) +
right('0000'+coalesce(parsename(v1, 3), '', 4)) +
right('0000'+coalesce(parsename(v1, 2), '', 4)) +
right('0000'+coalesce(parsename(v1, 1), '', 4))
) <
(right('0000'+coalesce(parsename(v2, 4), '', 4)) +
right('0000'+coalesce(parsename(v2, 3), '', 4)) +
right('0000'+coalesce(parsename(v2, 2), '', 4)) +
right('0000'+coalesce(parsename(v2, 1), '', 4))
)
then -1
when v1 = v2
then 0
else 1
end) as Comparison
Note that parsename() only works on up to four-parts in the name.
If you just want to sort, then the following will work:
order by (right('0000'+coalesce(parsename(v1, 4), '', 4)) +
right('0000'+coalesce(parsename(v1, 3), '', 4)) +
right('0000'+coalesce(parsename(v1, 2), '', 4)) +
right('0000'+coalesce(parsename(v1, 1), '', 4))
)

Replace values in a CSV string

I have a list of products in comma separated fashion and since the item list was replaced with new product items, I am trying to modify this CSV list with new product item list.
create table #tmp
(
id int identity(1,1) not null,
plist varchar(max) null
);
create table #tmpprod
(
oldid int null,
newid int null
);
insert into #tmp(plist) values
('10,11,15,17,19'),
('22,34,44,25'),
('5,6,8,9');
insert into #tmpprod(oldid, newid) values
(5, 109),
(9, 110),
(10, 111),
(15, 112),
(19, 113),
(30, 114),
(34, 222),
(44, 333);
I am trying to use a split fn to convert into rows and then replace these values and then convert columns to rows again. Is it possible in any other manner?
The output will be as:
id
newlist
1
111,11,112,17,113
2
22,222,333,25
3
109,6,8,110
Convert your comma separated list to XML. Use a numbers table, XQuery and position() to get the separate ID's with the position they have in the string. Build the comma separated string using the for xml path('') trick with a left outer join to #tempprod and order by position().
;with C as
(
select T.id,
N.number as Pos,
X.PList.value('(/i[position()=sql:column("N.Number")])[1]', 'int') as PID
from #tmp as T
cross apply (select cast('<i>'+replace(plist, ',', '</i><i>')+'</i>' as xml)) as X(PList)
inner join master..spt_values as N
on N.number between 1 and X.PList.value('count(/i)', 'int')
where N.type = 'P'
)
select C1.id,
stuff((select ','+cast(coalesce(T.newid, C2.PID) as varchar(10))
from C as C2
left outer join #tmpprod as T
on C2.PID = T.oldid
where C1.id = C2.id
order by C2.Pos
for xml path(''), type).value('.', 'varchar(max)'), 1, 1, '')
from C as C1
group by C1.id
Try on SE-Data
Assuming SQL Server 2005 or better, and assuming order isn't important, then given this split function:
CREATE FUNCTION [dbo].[SplitInts]
(
#List VARCHAR(MAX),
#Delimiter CHAR(1)
)
RETURNS TABLE
AS
RETURN ( SELECT Item FROM ( SELECT Item = x.i.value('(./text())[1]', 'int')
FROM
( SELECT [XML] = CONVERT(XML, '<i>' + REPLACE(#List, #Delimiter, '</i><i>')
+ '</i>').query('.') ) AS a CROSS APPLY [XML].nodes('i') AS x(i)
) AS y WHERE Item IS NOT NULL);
GO
You can get this result in the following way:
;WITH x AS
(
SELECT id, item, oldid, [newid], rn = ROW_NUMBER() OVER
(PARTITION BY id
ORDER BY PATINDEX('%,' + RTRIM(s.Item) + ',%', ',' + t.plist + ','))
FROM #tmp AS t CROSS APPLY dbo.SplitInts(t.plist, ',') AS s
LEFT OUTER JOIN #tmpprod AS p ON p.oldid = s.Item
)
SELECT id, newlist = STUFF((SELECT ',' + RTRIM(COALESCE([newid], Item))
FROM x AS x2 WHERE x2.id = x.id
FOR XML PATH(''),
TYPE).value(N'./text()[1]', N'varchar(max)'), 1, 1, '')
FROM x GROUP BY id;
Results:
id
newlist
1
111,11,112,17,113
2
22,222,333,25
3
109,6,8,110
Note that the ROW_NUMBER() / OVER / PARTITION BY / ORDER BY is only there to try to coerce the optimizer to return the rows in that order. You may observe this behavior today and it can change tomorrow depending on statistics or data changes, optimizer changes (service packs, CUs, upgrade, etc.) or other variables.
Long story short: if you're depending on that order, just send the set back to the client, and have the client construct the comma-delimited list. It's probably where this functionality belongs anyway.
That said, in SQL Server 2017+, we can guarantee retaining the order by splitting with OPENJSON() and reassembling with STRING_AGG():
;WITH x AS
(
SELECT o.id, val = COALESCE(n.newid, p.value), p.[key]
FROM #tmp AS o CROSS APPLY
OPENJSON('["' + REPLACE(o.pList, ',', '","') + '"]') AS p
LEFT OUTER JOIN #tmpprod AS n
ON p.value = n.oldid
)
SELECT id, newlist = STRING_AGG(val, ',')
WITHIN GROUP (ORDER BY [key])
FROM x GROUP BY id;
Example db<>fiddle
Thanks for this question - I've just learned something new. The following code is an adaptation of an article written by Rob Volk on exactly this topic. This is a very clever query! I won't copy all of the content down here. I have adapted it to create the results you're looking for in your example.
CREATE TABLE #nums (n INT)
DECLARE #i INT
SET #i = 1
WHILE #i < 8000
BEGIN
INSERT #nums VALUES(#i)
SET #i = #i + 1
END
CREATE TABLE #tmp (
id INT IDENTITY(1,1) not null,
plist VARCHAR(MAX) null
)
INSERT INTO #tmp
VALUES('10,11,15,17,19'),('22,34,44,25'),('5,6,8,9')
CREATE TABLE #tmpprod (
oldid INT NULL,
newid INT NULL
)
INSERT INTO #tmpprod VALUES(5, 109),(9, 110),(10, 111),(15, 112),(19, 113),(30, 114),(34, 222),(44, 333)
;WITH cte AS (SELECT ID, NULLIF(SUBSTRING(',' + plist + ',' , n , CHARINDEX(',' , ',' + plist + ',' , n) - n) , '') AS prod
FROM #nums, #tmp
WHERE ID <= LEN(',' + plist + ',') AND SUBSTRING(',' + plist + ',' , n - 1, 1) = ','
AND CHARINDEX(',' , ',' + plist + ',' , n) - n > 0)
UPDATE t SET plist = (SELECT CAST(CASE WHEN tp.oldid IS NULL THEN cte.prod ELSE tp.newid END AS VARCHAR) + ','
FROM cte LEFT JOIN #tmpprod tp ON cte.prod = tp.oldid
WHERE cte.id = t.id FOR XML PATH(''))
FROM #tmp t WHERE id = t.id
UPDATE #tmp SET plist = SUBSTRING(plist, 1, LEN(plist) -1)
WHERE LEN(plist) > 0 AND SUBSTRING(plist, LEN(plist), 1) = ','
SELECT * FROM #tmp
DROP TABLE #tmp
DROP TABLE #tmpprod
DROP TABLE #nums
The #nums table is a table of sequential integers, the length of which must be greater than the longest CSV you have in your table. The first 8 lines of the script create this table and populate it. Then I've copied in your code, followed by the meat of this query - the very clever single-query parser, described in more detail in the article pointed to above. The common table expression (WITH cte...) does the parsing, and the update script recompiles the results into CSV and updates #tmp.
Adam Machanic's blog contains this posting of a T-SQL only UDF which can accept T-SQL's wildcards for use in replacement.
http://dataeducation.com/splitting-a-string-of-unlimited-length/
For my own use, I adjusted the varchar sizes to max. Also note that this UDF performs rather slowly, but if you cannot use the CLR, it may be an option. The minor changes I made to the author's code may limit use of this to SQL Server 2008r2 and later.
CREATE FUNCTION dbo.PatternReplace
(
#InputString VARCHAR(max),
#Pattern VARCHAR(max),
#ReplaceText VARCHAR(max)
)
RETURNS VARCHAR(max)
AS
BEGIN
DECLARE #Result VARCHAR(max) = ''
-- First character in a match
DECLARE #First INT
-- Next character to start search on
DECLARE #Next INT = 1
-- Length of the total string -- 0 if #InputString is NULL
DECLARE #Len INT = COALESCE(LEN(#InputString), 0)
-- End of a pattern
DECLARE #EndPattern INT
WHILE (#Next <= #Len)
BEGIN
SET #First = PATINDEX('%' + #Pattern + '%', SUBSTRING(#InputString, #Next, #Len))
IF COALESCE(#First, 0) = 0 --no match - return
BEGIN
SET #Result = #Result +
CASE --return NULL, just like REPLACE, if inputs are NULL
WHEN #InputString IS NULL
OR #Pattern IS NULL
OR #ReplaceText IS NULL THEN NULL
ELSE SUBSTRING(#InputString, #Next, #Len)
END
BREAK
END
ELSE
BEGIN
-- Concatenate characters before the match to the result
SET #Result = #Result + SUBSTRING(#InputString, #Next, #First - 1)
SET #Next = #Next + #First - 1
SET #EndPattern = 1
-- Find start of end pattern range
WHILE PATINDEX(#Pattern, SUBSTRING(#InputString, #Next, #EndPattern)) = 0
SET #EndPattern = #EndPattern + 1
-- Find end of pattern range
WHILE PATINDEX(#Pattern, SUBSTRING(#InputString, #Next, #EndPattern)) > 0
AND #Len >= (#Next + #EndPattern - 1)
SET #EndPattern = #EndPattern + 1
--Either at the end of the pattern or #Next + #EndPattern = #Len
SET #Result = #Result + #ReplaceText
SET #Next = #Next + #EndPattern - 1
END
END
RETURN(#Result)
END