How to calculate superscript values in SQL - 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.

Related

Error Handling for numbers of delimiters when extracting substrings

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

Leading 0 on int Column problem SQL Server

I have an issue where I am trying to add a leading 0 to run an output.
SELECT
CASE
WHEN LEN(t.trans_time) = 5
THEN CONCAT(0, [trans_time])
ELSE T.[trans_time]
END AS [TransactionTime]
,RIGHT(CONCAT(0,trans_time),6) AS trans_time
,LEN(T.trans_Time)
,t.trans_time
Why does the case statement not return the leading 0 whereas using:
,RIGHT(CONCAT(0,trans_time),6) AS trans_time
Works no problem.
Case expression return only one type, whereas concat() would return different type & i am assuming trans_time has INT type.
So, you would need to do type conversations :
SELECT (CASE WHEN LEN(t.trans_time) = 5
THEN CONCAT(0, [trans_time])
ELSE CAST(T.[trans_time] AS VARCHAR(255))
END) AS [TransactionTime],
. . .
Another way to do this is to use the format function, wich is available from sql server 2012.
It not only makes the code more readable but will also perform better.
declare #t table (id int)
insert into #t values (90113), (90204), (90207), (90235), (90302), (90318), (90324)
select format(id, '000000') as TransactionTime from #t
this will return
TransactionTime
---------------
090113
090204
090207
090235
090302
090318
090324

"Insert Into Select" writing to table but contains sub-query reading from same table

I am adding records into my table "SampleTestLimits" using an "Insert Into Select", but which also has a sub-query reading from the same table to perform a count for me.
I don't think the sub-query is seeing the earlier records added by my "Insert Into Select". It's the same for Oracle and SQL Server. The code for SQL Server is shown below (my sub-query begins with "SELECT COALESCE...").
I have another stored procedure which does work in a similar situation.
Would appreciate it if anybody could tell if what I'm doing is a no no.
ALTER PROCEDURE [dbo].[CreateSampleTestLimits]
#SampleCode as NVARCHAR(80),
#TestPosition as smallint,
#TestCode NVARCHAR(20),
#TestVersion smallint,
#EnterDate as integer,
#EnterTime as smallint,
#EnterUser as NVARCHAR(50)
AS
BEGIN
INSERT INTO SampleTestLimits
([AuditNumber]
,[LimitNumber]
,[ComponentRow]
,[ComponentColumn]
,[ComponentName]
,[TestPosition]
,[SampleCode]
,[AuditFlag]
,[LimitSource]
,[LimitType]
,[UpperLimitEntered]
,[UpperLimitValue]
,[LowerLimitEntered]
,[LowerLimitValue]
,[LimitTextColour]
,[LimitPattern]
,[LimitForeColour]
,[LimitBackColour]
,[CreatedDate]
,[CreatedTime]
,[CreatedUser]
,[LimitText]
,[FilterName]
,[deleted]
,IsRuleBased)
SELECT 1 --starting auditnumber
,(SELECT COALESCE(MAX(LimitNumber), 0) + 1 AS NextLimitNumber FROM SampleTestLimits WHERE SampleCode=#SampleCode AND TestPosition=#TestPosition AND ComponentRow=1 AND ComponentColumn=1 AND AuditFlag=0) -- TFS bug# 3952: Calculate next limit number.
,ComponentRow
,ComponentColumn
,(select ComponentName from TestComponents TC where TC.TestCode=#TestCode and TC.ComponentColumn=TestLimits.ComponentColumn and TC.ComponentRow = TestLimits.ComponentRow and TC.AuditNumber=TestLimits.AuditNumber)
,#TestPosition
,#SampleCode
,0 --auditflag
,1 --limitsource = test
,[LimitType]
,[UpperLimitEntered]
,[UpperLimitValue]
,[LowerLimitEntered]
,[LowerLimitValue]
,[LimitTextColour]
,[LimitPattern]
,[LimitForeColour]
,[LimitBackColour]
,#EnterDate
,#EnterTime
,#EnterUser
,[LimitText]
,[FilterName]
,0 --deleted
,0 --rule based
FROM TestLimits join Tests on Tests.TestCode=TestLimits.TestCode and Tests.AuditNumber= TestLimits.AuditNumber WHERE Tests.TestCode=#TestCode and Tests.auditnumber=#TestVersion and ([TestLimits].FilterString is null or DATALENGTH([TestLimits].FilterString)=0)
END
Assuming that I understand your logic correctly (ie. that you want the nextlimitnumber to increase by 1 for each row being added), in Oracle, I'd do it by using the analytic function row_number() to work out what number to add to the previous max value, something like:
INSERT INTO sampletestlimits (auditnumber,
limitnumber,
componentrow,
componentcolumn,
componentname,
testposition,
samplecode,
auditflag,
limitsource,
limittype,
upperlimitentered,
upperlimitvalue,
lowerlimitentered,
lowerlimitvalue,
limittextcolour,
limitpattern,
limitforecolour,
limitbackcolour,
createddate,
createdtime,
createduser,
limittext,
filtername,
deleted,
isrulebased)
SELECT 1, --starting auditnumber
(SELECT COALESCE (MAX (limitnumber), 0) + 1 AS nextlimitnumber
FROM sampletestlimits
WHERE samplecode = p_samplecode
AND testposition = p_testposition
AND componentrow = 1
AND componentcolumn = 1
AND auditflag = 0)
+ row_number() over (partition by testposition, componentrow, componentcolumn, auditflag) as nextlimitnumber, -- TFS bug# 3952: Calculate next limit number.
componentrow,
componentcolumn,
(SELECT componentname
FROM testcomponents tc
WHERE tc.testcode = p_testcode
AND tc.componentcolumn = testlimits.componentcolumn
AND tc.componentrow = testlimits.componentrow
AND tc.auditnumber = testlimits.auditnumber),
p_testposition,
p_samplecode,
0, --auditflag
1, --limitsource = test
limittype,
upperlimitentered,
upperlimitvalue,
lowerlimitentered,
lowerlimitvalue,
limittextcolour,
limitpattern,
limitforecolour,
limitbackcolour,
p_enterdate,
p_entertime,
p_enteruser,
limittext,
filtername,
0, --deleted
0 --rule based
FROM testlimits
JOIN tests
ON tests.testcode = testlimits.testcode
AND tests.auditnumber = testlimits.auditnumber
WHERE tests.testcode = p_testcode
AND tests.auditnumber = p_testversion
AND ( testlimits.filterstring IS NULL
OR datalength (testlimits.filterstring) = 0);
I had to guess at what the partition by clause would need to contain - adjust that as necessary for your requirements.

How can I structure an IF statement inside of a SELECT statement?

I'm hoping that what I have paints a clear enough picture of what I am trying to accomplish:
SELECT [Date]
,[ChargeCode]
,[ChargeDescription]
,[HHY_Qty]
,[PatPrice]
, IF ISNUMERIC(HHY_Qty) AND ISNUMERIC(PatPrice)
BEGIN
CAST(HHY_Qty AS INT) * CAST(PatPrice AS INT) AS ExtAmt
END
ELSE
0 AS ExtAmt
END
FROM [dbo].[ChargeDetails]
WHERE PatientNumber = '1271'
HHY_Qty and PatPrice are both VARCHAR types in a MSSQL database. They were created with a BULK INSERT from a very very very dirty CSV from an AS400 export. Here, I am trying to do some multiplication IF the fields are numeric values, otherwise ExtAmt should be 0. Is that possible? If not,, is there a workaround?
Use a CASE statement:
CASE WHEN ISNUMERIC(HHY_Qty) = 1 AND ISNUMERIC(PatPrice) = 1 THEN CAST(HHY_Qty AS INT) * CAST(PatPrice AS INT) ELSE 0 END AS ExtAmt

Return all results of substring with in string

I have some odd data in a vendor database but need to be able to extract multiple different parameters from one field in the db.
So from this example i would like to pull out all items that fall between (" % ")
Between quotes is a string, disregard that it looks like code:
"Func_GetParameterLatestValue("IBW Patient Height RT Assess") kHeight =Func_GetParameterLatestValue("Height For IBW Vent Misc") If (kSex) = "" Then
Return_Value =NULL Else If kHeight > 0 Then If kSex=1 Then Return_Value= Round(((kHeight - 152.4)*.91)+50,0) Else
Return_Value= Round(((kHeight - 152.4)*.91)+45.5,0) End IF Else Return_Value = NULL End IF End IF ' Return_Value = kHeight '("IBW Patient Height RT Assess")"
so the return values would be:
IBW Patient Height RT Assess,
Height For IBW Vent Misc,
IBW Patient Height RT Assess
Im open to any suggestions to try and make this work. Ideally i would like to be able to slam the results in a subquery as well to make sure that they exist on another table.
This query currently returns the first instance
select vbs.Name,
SUBSTRING(sd.FormulaDetails,
CHARINDEX('("', sd.FormulaDetails)+2,(CHARINDEX('")',sd.FormulaDetails) - CHARINDEX('("', sd.FormulaDetails))-2)
from StatementDefinitions sd, MvVBScript vbs
where sd.ScriptID = vbs.ID
You can do this recursively with a WITH statement. Here's a shot at it. Change varchar(max) to whatever the data type of your FormulaDetails column is. In case you want it, this query returns the ScriptID and numbers the position of the chunk it finds (so 'Height For IBW Vent Misc' would be occurrence 2)
with Chunks(id,occurrence,position,token,remainder) as (
select
ScriptID,
cast(0 as int),
charindex(#token,FormulaDetails),
cast('' as varchar(max)),
substring(c,charindex(#token,FormulaDetails)+1,len(FormulaDetails))
from StatementDefinitions
where FormulaDetails like '%'+#token+'%'
union all
select
id,
occurrence+1,
charindex(#token,remainder)+position,
cast(substring(remainder,1,charindex(#token,remainder)-1) as varchar(max)),
substring(remainder,charindex(#token,remainder)+1,len(remainder))
from Chunks
where remainder like '%'+#token+'%'
)
select id, occurrence, token from Chunks
where occurrence > 0
order by id;