Error Handling for numbers of delimiters when extracting substrings - sql

Situation: I have a column where each cell can have up to 5 delimiters. However, it's possible that there are none.
Objective: How do i handle errors such as :
Invalid length parameter passed to the LEFT or SUBSTRING function.
in the case that it cannot find the specified delimiter.
Query:
declare #text VARCHAR(111) = 'abc-def-geeee-ifjf-zzz'
declare #start1 as int
declare #start2 as int
declare #start3 as int
declare #start4 as int
declare #start_index_reverse as int
set #start1 = CHARINDEX('-',#text,1)
set #start2 = CHARINDEX('-',#text,charindex('-',#text,1)+1)
set #start3 = CHARINDEX('-',#text,charindex('-',#text,CHARINDEX('-',#text,1)+1)+1)
set #start4 = CHARINDEX('-',#text,charindex('-',#text,CHARINDEX('-',#text,CHARINDEX('-',#text,1)+1)+1)+1)
set #start_index_reverse = CHARINDEX('-',REVERSE(#text),1)
select
LEFT(#text,#start1-1) AS Frst,
SUBSTRING(#text,#start1+1,#start2-#start1-1) AS Scnd,
SUBSTRING(#text,#start2+1,#start3-#start2-1) AS Third,
SUBSTRING(#text,#start3+1,#start4-#start3-1)AS Third,
RIGHT(#text,#start_index_reverse-1) AS Lst
In this case my variable includes 5 delimiters and so my query works but if i removed one '-' it would break.

XML support in SQL Server brings about some unintentional but useful tricks. Converting this string to XML allows for some parsing that is far less messy than native string handling, which is very far from awesome.
DECLARE #test varchar(111) = 'abc-def-ghi-jkl-mnop'; -- try also with 'abc-def'
;WITH n(x) AS
(
SELECT CONVERT(xml, '<x>' + REPLACE(#test, '-', '</x><x>') + '</x>')
)
SELECT
Frst = x.value('/x[1]','varchar(111)'),
Scnd = x.value('/x[2]','varchar(111)'),
Thrd = x.value('/x[3]','varchar(111)'),
Frth = x.value('/x[4]','varchar(111)'),
Ffth = x.value('/x[5]','varchar(111)')
FROM n;
For a table it's almost identical:
DECLARE #foo TABLE ( col varchar(111) );
INSERT #foo(col) VALUES('abc-def-ghi-jkl-mnop'),('abc'),('def-ghi');
;WITH n(x) AS
(
SELECT CONVERT(xml, '<x>' + REPLACE(col, '-', '</x><x>') + '</x>')
FROM #foo
)
SELECT
Frst = x.value('/x[1]','varchar(111)'),
Scnd = x.value('/x[2]','varchar(111)'),
Thrd = x.value('/x[3]','varchar(111)'),
Frth = x.value('/x[4]','varchar(111)'),
Ffth = x.value('/x[5]','varchar(111)')
FROM n;
Results (sorry about the massive size, seems this doesn't handle 144dpi well):

add a test before your last select
then you should decide how to handle the other case (when one of start is 0)
You can also refer to this link about splitting a string in sql server
which is uses a loop and can handle any number of delimiters
if #start1>0 and #start2>0 and #start3>0 and #start4>0
select LEFT(#text,#start1-1) AS Frst,
SUBSTRING(#text,#start1+1,#start2-#start1-1) AS Scnd,
SUBSTRING(#text,#start2+1,#start3-#start2-1) AS Third,
SUBSTRING(#text,#start3+1,#start4-#start3-1)AS Third,
RIGHT(#text,#start_index_reverse-1) AS Lst

Related

How to calculate superscript values in SQL

I have prices coming in my source file like below -
78-22¼,
78-18⅝
I need to calculate these price. For example for first case result should be 78-22.25. I searched a lot but found that SQL supports few of these characters only. Is there anyway to make sure we are able to calculate for whatever value we are getting. Solution in either SQL or PowerShell could work.
You could write a PowerShell function to convert the fractions to decimals:
PS ~> ConvertTo-Decimal '18⅝'
18.625
To do so, we'll need to write a function that:
Uses regex to identify and extract the integer and fraction parts
Uses [char]::GetNumericValue() to get the decimal representation of the fraction
Outputs the sum of the two
function ConvertTo-Decimal {
param(
[Parameter(Mandatory)]
[string]$InputObject
)
if($InputObject -match '^(-?)(\d+)(\p{No})?$'){
$baseValue = +$Matches[2]
if($Matches[3]){
$baseValue += [char]::GetNumericValue($Matches[3])
}
if($Matches[1] -eq '-'){
$baseValue *= -1
}
return $baseValue
}
return 0
}
Hoo this one was fun.
If you want to do it purley in TSQL give this a tug:
DECLARE #table TABLE (Glyph NVARCHAR(2), Dec DECIMAL(8,6))
INSERT INTO #table (Glyph, Dec) VALUES
(N'¼', 1.0/4),(N'½', 1.0/2),(N'¾', 3.0/4),(N'⅐', 1.0/7),
(N'⅑', 1.0/8),(N'⅒',1.0/10),(N'⅓', 1.0/3),(N'⅔', 2.0/3),
(N'⅕', 1.0/5),(N'⅖', 2.0/5),(N'⅗', 3.0/5),(N'⅘', 4.0/5),
(N'⅙', 1.0/6),(N'⅚', 5.0/6),(N'⅛', 1.0/8),(N'⅜', 3.0/8),
(N'⅝', 5.0/8),(N'⅞', 7.0/8),(N'⅟', 1.0/1)
DECLARE #values TABLE (ID INT IDENTITY, value NVARCHAR(20))
INSERT INTO #values (value) VALUES
(N'78-22¼'),(N'78-18⅝'),(N'10+1')
;WITH sort AS (
SELECT v.*, t.*,
CASE WHEN m.value = v.value THEN
CASE WHEN t.Dec IS NOT NULL THEN REPLACE(p.value,t.Glyph,'')+dec
ELSE p.value
END
ELSE
CASE WHEN t.Dec IS NOT NULL THEN REPLACE(m.value,t.Glyph,'')+dec
ELSE m.value
END
END AS v,
CASE WHEN m.value = v.value THEN '+'
ELSE '-' END AS op,
ROW_NUMBER() OVER (PARTITION BY v.value ORDER BY CASE WHEN m.value = v.value THEN CHARINDEX(m.value,v.value) ELSE CHARINDEX(p.value,v.value) END) AS subID
FROM #values v
OUTER APPLY STRING_SPLIT(v.value,'-') m
OUTER APPLY STRING_SPLIT(v.value,'+') p
LEFT OUTER JOIN #table t
ON RIGHT(CASE WHEN m.value = v.value THEN p.value ELSE m.value END,1) = t.Glyph
)
SELECT ID, value, SUM(v * CASE WHEN subId = 1 THEN 1 WHEN op = '+' THEN 1 ELSE -1 END) AS v
FROM sort
GROUP BY ID, value
ID value v
---------------------
1 78-22¼ 55.750000
2 78-18⅝ 59.375000
3 10+1 11.000000
#values replaces your table.
disclaimer: this works, it'll probably perform like hot garbage, but it works :P
In T-SQL you could write a function like this that takes a vulgar fraction and replaces it with its decimal equivalent (this is not completely exhaustive, but handles the most common fractions, and makes a guess about whether you want .666 or .667 or something else for ⅔):
CREATE FUNCTION dbo.FractionToDecimal(#str nvarchar(255))
RETURNS TABLE
AS
RETURN
(
SELECT str = REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(
REPLACE(REPLACE(REPLACE(REPLACE(#str, N'¼','.25'),
N'½','.5'), N'¾','.75'), N'⅓','.333'), N'⅔','.666'),
N'⅛','.125'), N'⅜','.375'), N'⅝','.625'), N'⅞','.875')
);
Sample usage:
DECLARE #x table(str nvarchar(255));
INSERT #x VALUES(N'78-22¼'),(N'78-18⅝');
SELECT [input] = i.str, [output] = o.str
FROM #x AS i
CROSS APPLY dbo.FractionToDecimal(str) AS o;
Output:
input
output
78-22¼
78-22.25
78-18⅝
78-18.625
Working example in this fiddle.
Note there are only so many fraction codes available that you could be pulling in ... so you could add any to the above e.g. from this set but it isn't the case that you would have to handle any possible fraction, only the ones that are represented by these specific symbols. If someone passes in plain text 9/10 or 31/33 that is a completely different problem space than what you have outlined in the question.

Find a string inside a string and update in SQL Server

I have a table like this :
create table Fbank (id int, COCODE nvarchar(max))
insert into Fbank
values (1, '<row xml:space="preserve" id="174580000041250.040002">
<c1>HTG115960001</c1>
<c2>14013514,48</c2>
<c3>222</c3>
<c4>BAL MIGRATED</c4>
<c5>NULL</c5>
<c6>NULL</c6>
<c7>NULL</c7>
<c8>9900</c8>
<c9>11596</c9>
<c10>20151017</c10>
<c11>HTG</c11>
<c12>NULL</c12>
<c13>NULL</c13>
<c14>FT1529010083</c14>
<c15>1</c15>
<c16>FT1529010083</c16>
<c17>FT</c17>
<c18>20151017</c18>
<c19>NULL</c19>
<c20>0</c20>
<c21>5033_INPUTTER__OFS_AUTO.FT</c21>
<c22>5033_INPUTTER_OFS_AUTO.FT</c22>
<c23>1510181127</c23>
<c24>HG0010001</c24>
</row>')
I need to replace the string inside the tag c24 tag..
Excepted output: I need to replace the letter 'G' to be replaced with 'T' like this inside c24 tag - HT0010001 and all the rest should be same unchanged...
What I tired:
declare #searchText varchar(20) = (
select SUBSTRING(COCODE,CHARINDEX('<c24>', COCODE), 7)
from FBNK_EB_SFF_ACCT_HI000)
update Fbank
set cocode = STUFF(#searchText, CHARINDEX('<c24>', COCODE), 1, 'T')
If the code always begins with 'H', then I imagine it's easier to just have '<c24>H' be part of the string to replace, but then add it back again:
update Fbank
set COCODE = replace(cocode, '<c24>HG', '<c24>HT');
If it doesn't, then I'd say you're on the right track with charindex and substring. But use patindex instead, and update fbank directly, don't put the search term into any variable. If you're trying to split up your logic into parts, consider cross apply:
update fb
set cocode = stuff(cocode, ix, 1, 'T')
from Fbank fb
cross apply (select ix = patindex('%<c24>%', cocode) + 6) ap
where substring(cocode, ix, 1) = 'G'

How can I write this SQL while loop code to get an XML results in one line instead of 3 separate lines?

I'm trying to get all this XML result in one line instead of 3 for each column
DECLARE #ii INT = 10;
DECLARE #String1 NVARCHAR(4000);
SET #String1 = '';
WHILE(#ii <= 18)
BEGIN
SET #String1 = (#String1 + 'SELECT LoanNumber = ''Complaint'+CONVERT(VARCHAR(2),#ii)+'-Call1'' , LoanStatus=''Compliants'' , LoanStatusDate = CAST(GETDATE() AS DATE)
UNION
SELECT LoanNumber = ''Complaint'+CONVERT(VARCHAR(2),#ii)+'-Call2'', LoanStatus=''Compliants'' , LoanStatusDate = CAST(GETDATE() AS DATE)
UNION
SELECT LoanNumber = ''Complaint'+CONVERT(VARCHAR(2),#ii)+'-Call3'', LoanStatus=''Compliants'' , LoanStatusDate = CAST(GETDATE() AS DATE)')
IF #ii != 18
SET #string1 = #string1 + ' UNION '
ELSE
SET #string1 = #string1 + 'FOR XML PATH (''Loan''),ROOT(''Loans'') '
SET #ii = #ii+1
END
EXEC sp_executesql #String1
I want something like this:
<Loans>
<LoanNumber>Complaint10-Call1<LoanStatus>Compliants<LoanStatusDate>2019-01-18
</Loan>
<Loan>
<LoanNumber>Complaint10-Call2 <LoanStatus>Compliants<LoanStatusDate>2019-01-18
</Loan>
<Loan>
<LoanNumber>Complaint10-Call3<LoanStatus>Compliants<LoanStatusDate>2019-01-18
</Loan>
Instead of the result that you get when you execute the code I provided. I appreciate your help.
This might be wild guessing, but I've got the feeling, that I understand, what this is about:
if you run the code you will see the result. no input data is needed .
I just want the structure of the xml outcome to all be on one line for
one set of each loop
Your provided code leads to this:
<Loans>
<Loan>
<LoanNumber>Complaint10-Call1</LoanNumber>
<LoanStatus>Compliants</LoanStatus>
<LoanStatusDate>2019-01-22</LoanStatusDate>
</Loan>
<Loan>
<LoanNumber>Complaint10-Call2</LoanNumber>
<LoanStatus>Compliants</LoanStatus>
<LoanStatusDate>2019-01-22</LoanStatusDate>
</Loan>
<!-- more of them-->
</Loans>
This is perfectly okay, valid XML.
But you want the result
outcome to all be on one line for one set of each loop
Something like this?
<Loans>
<Loan>
<LoanNumber>Complaint10-Call1</LoanNumber><LoanStatus>Compliants</LoanStatus><LoanStatusDate>2019-01-22</LoanStatusDate>
</Loan>
<!-- more of them-->
</Loans>
There is a big misconception I think... XML is not the thing you see. The same XML can look quite differently, without any semantic difference:
Check this out:
DECLARE #xmltable table(SomeXml XML)
INSERT INTO #xmltable VALUES
--the whole in one line
('<root><a>test</a><a>test2</a></root>')
--all <a>s in one line
,('<root>
<a>test</a><a>test2</a>
</root>')
--each element in one line
,('<root>
<a>test</a>
<a>test2</a>
</root>')
--white space going wild...
,('<root>
<a>test</a>
<a>test2</a>
</root>');
--now check the results
SELECT * FROM #xmltable;
This means: How the XML appears is a matter of the interpreter. The same XML opened with another tool might appear differently. Dealing with XML means dealing with data but not with format... The actual format has no meaning and should not matter at all...
Starting with SQL-Server 2016 you might have a look at JSON, if you need a tiny format:
DECLARE #somedata table(SomeValue VARCHAR(100),SomeStatus VARCHAR(100),SomeDate DATE);
INSERT INTO #somedata VALUES
('Complaint10-Call1','Complaints','2019-01-22')
,('Complaint10-Call2','Complaints','2019-01-22')
,('Complaint10-Call3','Complaints','2019-01-22');
SELECT * FROM #somedata FOR JSON PATH;
The result comes in one line:
[{"SomeValue":"Complaint10-Call1","SomeStatus":"Complaints","SomeDate":"2019-01-22"},{"SomeValue":"Complaint10-Call2","SomeStatus":"Complaints","SomeDate":"2019-01-22"},{"SomeValue":"Complaint10-Call3","SomeStatus":"Complaints","SomeDate":"2019-01-22"}]

SQL server update fields from a concatenated string in a field

I'm fairly new to SQL code which writes to the database. I've been trying to work out this piece of code on my own, but I'm not having a lot of luck. Especially since I really don't know how to test it without actually writing to the DB
I have a database with 5 UDF fields. 'UDF1-UDF5'. The operators at my facility are supposed to scan a bar code into a specific bar code field which splits up the bar code into the five fields (they are all char(30) fields ). Unfortunately what is happening is that they are scanning directly into the UDF1 field, so the entire barcode string is all in one field. (I don't have control over this software) I am trying to write a script which will parse the DB, split these fields into separate variables and update the DB. I could use a little assistance because I think I need Dynamic SQL to do this and I don't know much about it. Here is a little more info about the system.
The barcode field looks like this:
%2S12345%1%1%0%10%
where the '%' characters begin and end the bar code and concatenate the characters. the first character of the first UDF field '2' is a check digit, and always the same.
The first field is always either 5 or 6 characters (excluding the check digit), the rest are either 1 or 2 digits. I also need code that won't break if the bar code only has the first three fields. Not a lot of consistency here. some of the bar codes are truncated.
questions,
As far as I know, the only way to break apart concatenated text is substring() which is position based, so I would need an additional 5 variables to get the length of each field and a way to query that information. Is there an easier way?
At some point I have to conditionally set the variables and I can't seem to get set commands to work. I understand why something like this doesn't work, but I don't know any other way of doing it.
.
DECLARE #BASEID CHAR(30), #LOTID CHAR(30), #SPLITID CHAR(30), #SUBID CHAR(30), #SEQUENCENO CHAR(30), #BASELEN INT
SET #BASELEN =
CASE WHEN(
SELECT ISNUMERIC(SUBSTRING(R.UDF1,3,1))
FROM VISION17SLITTER.DBO.ROLLINFO R
WHERE R.UDF1 LIKE '[%]%'
) = 1
THEN 5
ELSE 6
END
3. once I could get the variable set I assume that a simple conditional update statement would work, but if there is anything else I should know before trying this I would appreciate the advice.
Thanks again,
Dan
Consider the following:
Declare #YourTable table (ID int,BarCode varchar(100))
Insert Into #YourTable values
(1,'%2S12345%1%1%0%10%'),
(2,'%ABC1234%2%3%4%50%')
Select A.ID
,A.BarCode
,B.*
From #YourTable A
Cross Apply (
Select Pos1 = xDim.value('/x[1]','varchar(max)')
,Pos2 = xDim.value('/x[2]','varchar(max)')
,Pos3 = xDim.value('/x[3]','varchar(max)')
,Pos4 = xDim.value('/x[4]','varchar(max)')
,Pos5 = xDim.value('/x[5]','varchar(max)')
,Pos6 = xDim.value('/x[6]','varchar(max)')
,Pos7 = xDim.value('/x[7]','varchar(max)')
,Pos8 = xDim.value('/x[8]','varchar(max)')
,Pos9 = xDim.value('/x[9]','varchar(max)')
From (Select Cast('<x>' + Replace(A.BarCode,'%','</x><x>')+'</x>' as XML) as xDim) A
) B
Returns
Now, you may notice Pos1 and Pos7 are blank. This is due to the fact that your string begins and ends with the delimiter. If you want to tailor the CROSS APPLY as such:
Select Pos1 = xDim.value('/x[2]','varchar(max)')
,Pos2 = xDim.value('/x[3]','varchar(max)')
,Pos3 = xDim.value('/x[4]','varchar(max)')
,Pos4 = xDim.value('/x[5]','varchar(max)')
,Pos5 = xDim.value('/x[6]','varchar(max)')
From (Select Cast('<x>' + Replace(A.BarCode,'%','</x><x>')+'</x>' as XML) as xDim) A
Which Returns
Create a function to split your string
CREATE FUNCTION [dbo].[fnSplitString]
(
#string NVARCHAR(MAX),
#delimiter CHAR(1)
)
RETURNS #output TABLE(splitdata NVARCHAR(MAX)
)
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
Then remove the first two control characters, and the last control character and invoke the function like this
select * from dbo.fnSplitString('S12345%1%1%0%10','%')
The function will then return a table with the following values:
splitdata
=========
S1234
1
1
0
10

SQL Server 2008 math fail

After hunting around on various forums for almost an hour, I've come to the conclusion that SQL server is slightly stupid about simple arithmetic.
I am attempting to utilize a function which, until recently seemed to work just fine. Upon changing out some of the values for a different set of information on the form in use, I get the odd behavior ahead.
The problem is that it is giving me the incorrect result as based on an excel spreadsheet formula.
The formula looks like this:
=IF(D8=0,0,(((D8*C12-C16)*(100-C13)/100+C16)/D8)+(C18*D8))
My SQL looks like this:
(((#DaysBilled * #ContractRate - #ActualPlanDed) * (100 - #InsCover) / 100 + #ActualPlanDed) / #DaysBilled) + (#CoPay * #DaysBilled)
Filling the variables with the given data looks like this:
(((11 * 433 - 15) * (100 - 344) / 100 + 15) / 11) + (15 * 11)
Even stranger, if I use the numbers above (adding .00 to the end of each value) manually in the server environment, it gives me -11405.1200000000
With the values I am giving, it should come out 166.36. Unfortunately, I am getting -886.83
Here is the entire function and how it is called:
ALTER FUNCTION Liability
(
#ClientGUID CHAR(32),
#RecordGUID CHAR(32),
#Type CHAR(3)
)
RETURNS DECIMAL(18,2) AS
BEGIN
DECLARE #ReturnValue decimal(18,2);
DECLARE #DaysBilled int;
DECLARE #ContractRate decimal(18,2);
DECLARE #ActualPlanDed decimal(18,2);
DECLARE #InsCover decimal(18,2);
DECLARE #CoPay decimal(18,2);
IF (#Type = 'RTC')
BEGIN
SELECT #DaysBilled = RTCDaysBilled,
#ContractRate = CAST(REPLACE(REPLACE(ContractRateRTC, ' ',''),'$', '') AS DECIMAL(6,2)),
#ActualPlanDed = RTCActualPlanDed,
#InsCover = InsRTCCover,
#CoPay = RTCCoPay
FROM AccountReconciliation1
WHERE #ClientGUID = tr_42b478f615484162b2391ef0b2c35ddc
AND #RecordGUID = tr_abb4effa0d9c4fe98c78cb4d2e21ba5d
END
IF (#Type = 'PHP')
BEGIN
SELECT #DaysBilled = PHPDaysBilled,
#ContractRate = CAST(REPLACE(REPLACE(ContractRatePHP, ' ',''),'$', '') AS DECIMAL(6,2)),
#ActualPlanDed = PHPActualPlanDed,
#InsCover = InsPHPCover,
#CoPay = PHPCoPay
FROM AccountReconciliation1
WHERE #ClientGUID = tr_42b478f615484162b2391ef0b2c35ddc
AND #RecordGUID = tr_abb4effa0d9c4fe98c78cb4d2e21ba5d
END
IF (#Type = 'IOP')
BEGIN
SELECT #DaysBilled = IOPDaysBilled,
#ContractRate = CAST(REPLACE(REPLACE(ContractRateIOP, ' ',''),'$', '') AS DECIMAL(6,2)),
#ActualPlanDed = IOPActualPlanDed,
#InsCover = InsIOPCover,
#CoPay = IOPCoPay
FROM AccountReconciliation1
WHERE #ClientGUID = tr_42b478f615484162b2391ef0b2c35ddc
AND #RecordGUID = tr_abb4effa0d9c4fe98c78cb4d2e21ba5d
END
IF (#DaysBilled <> 0)
BEGIN
SET #ReturnValue = (((#DaysBilled * #ContractRate - #ActualPlanDed)
*
(100 - #InsCover) / 100 + #ActualPlanDed)
/
#DaysBilled
)
+
(#CoPay * #DaysBilled)
END
ELSE
BEGIN
SET #ReturnValue = 0;
END
RETURN #ReturnValue;
END
It is called by running a select statement from our front-end, but the result is the same as calling the function from within management studio:
SELECT dbo.Liability('ClientID','RecordID','PHP') AS Liability
I have been reading about how a unary minus tends to break SQL's math handling, but I'm not entirely sure how to counteract it.
One last stupid trick with this function: It must remain a function. I cannot convert it into a stored procedure because it must be used with our front-end, which cannot utilize stored procedures.
Does SQL server even care about the parentheses? Or is it just ignoring them?
The calculation is correct, it differes of course if you are using float values
instead of integers.
For (((11 * 433 - 15) * (100 - 344) / 100 + 15) / 11) + (15 * 11)
a value around -886.xx depending in which places integers/floats are used is correct,
What makes you believe it should be 166.36?