Here is a definition of Navision CALCDATE function:
NewDate := CALCDATE(DateExpression [, Date])
It takes two parameters:
DateExpression which in Navision has type DateFormula which in SQL is stored as varchar(32)
Date, which in SQL means DateTime. To simplify the problem I'm assuming it's not optional (in Navsion, it is)
I need to access Navision data from SQL and do some calculations based on DateFormulas stored there.
I know how to write such function in SQL CLR, so please don't include this in your answer.
How to write CALCDATE function in TSQL?
[Optional] Is it possible to do it in SQL query only, using some (auxiliary) CTEs (this will allow me to not add function to Navsion database, therefore not modify it)?
Does SQL CLR is my only option?
Main problem I have with this function is that it seems that it must be done in following steps:
Parse DateExpression
Apply parsing result to Date parameter and produce new date.
But how to do the parsing part?
For example DateExpression = '-CW+1W+1D' means that we must do 3 things with our Date parameter in exactly this order:
Find last Monday (current week start) '-CW'
Add 1 week (7 days) '+1W'
Add 1 day '+1D'
It can be written in shorter form '+CW+1D', which means next monday and 1 day.
Generally this varchar(32) can contain 1 or more such expressions and they must be applied in provided order on Date parameter.
How to parse such varchar(32) in SQL?
In C# I would do it in a loop or recursively, but how to do it in SQL (sets)?
How to convert such structured varchar(32) into set of formulas.
Maybe I need to convert it somehow to XML and query it using SQL Server XML functions - but how?
EDIT:
How I want to use it?
I want to use this function as part of larger query.
For example (simplified):
select ...
from [Issued Reminder Header] as reminder
join [Reminder Terms] as terms on ...
join [Reminder Level] as level on ...
cross apply dbo.CalcDate(level.[Grace Period], reminder.[Posting Date]) as calculated
What I mean, is that level.[Grace Period] is in table that can be edited by users and can change any moment. So I cannot take any shortcuts and precalculate few values.
This answer is here primarily as an example in how silly this would be to try and implement in pure SQL (and also because I considered it an interesting challenge).
It is not intended to be used in any kind of production capacity and I am not even sure it properly implements the functionality of calcdate or would work across all the possible scenarios that calcdate allows.
SQL
declare #d date = '19960521';
declare #t table(Expression varchar(32),ExpectedValue date);
insert into #t values
('-CW+1W+1D','19960528')
,('CQ+1M-10D','19960720')
,('+CW+1D','19960528')
,('+CW+1WD','19960528')
,('CM+30D','19960630')
,('-WD2','19960514')
,('WD3','19960522')
,('-D3','19960503')
,('-D27','19960427')
,('D3','19960603')
,('D27','19960527')
,('10D','19960531')
,('1W','19960528')
,('W2','19970106')
,('-W2','19960108')
,('M12','19961201')
,('M2','19970201')
,('-M2','19960201')
,('Q4','19961001')
,('Q2','19970401')
,('-Q2','19960401')
;
with v as
(
select Expression
,ExpectedValue
,stuff(replace(replace(case when left(Expression,1) not in('+','-') then '+' else '' end + Expression
,'+','|+'
)
,'-','|-'
)
,1,1,''
) + '|||' as Fmt
from #t
)
,p as
(
select Expression
,ExpectedValue
,Fmt
,left(v.Fmt,charindex('|',v.Fmt,0)-1) as p1
,substring(v.Fmt
,charindex('|',v.Fmt,0)+1
,(charindex('|',v.Fmt,charindex('|',v.Fmt,0)+1)-1) - (charindex('|',v.Fmt,0))
) as p2
,replace(substring(v.Fmt
,charindex('|',v.Fmt,charindex('|',v.Fmt,0)+1)+1
,999
)
,'|'
,''
) as p3
from v
)
select #d as GivenDate
,p.Expression
,p.ExpectedValue
,cast(v3.retp3 as date) as ReturnedValue
,case when p.ExpectedValue = v3.retp3 then 'Match' else 'No Match' end as ValueCheck
from p
outer apply(values(case when substring(p1,2,1) = 'C' -- <Prefix><Unit>
then case substring(p1,3,2)
when 'D' then #d
when 'WD' then #d
when 'W' then dateadd(week, datediff(week,0,#d) + case when left(p1,1) = '+' then 1 else 0 end, 0)
when 'M' then case when left(p1,1) = '+' then eomonth(#d) else dateadd(day,1,eomonth(#d,-1)) end
when 'Q' then dateadd(day,case when left(p1,1) = '+' then -1 else 0 end,dateadd(quarter, datediff(quarter,0,#d) + case when left(p1,1) = '+' then 1 else 0 end, 0))
when 'Y' then dateadd(year, datediff(year,0,#d) + case when left(p1,1) = '+' then 1 else 0 end, 0)
else ''
end
when isnumeric(substring(p1,2,1)) = 1 -- <Number><Unit>
then case when right(p1,2) = 'WD'
then dateadd(day,cast(replace(p1,'WD','') as int),#d)
else case right(p1,1)
when 'D' then dateadd(day,cast(replace(p1,'D','') as int),#d)
when 'W' then dateadd(week,cast(replace(p1,'W','') as int),#d)
when 'M' then dateadd(month,cast(replace(p1,'M','') as int),#d)
when 'Q' then dateadd(quarter,cast(replace(p1,'Q','') as int),#d)
when 'Y' then dateadd(year,cast(replace(p1,'Y','') as int),#d)
end
end
when isnumeric(substring(p1,2,1)) = 0 -- <Unit><Number>
then case when substring(p1,2,2) = 'WD'
then dateadd(day,right(p1,1)-1,dateadd(week, datediff(week,0,#d) - case when left(p1,1) = '-' then 1 else 0 end, 0))
else case substring(p1,2,1)
when 'D' then dateadd(day,abs(cast(replace(p1,'D','') as int))-1,dateadd(month, datediff(month,0,#d) + case when abs(cast(replace(p1,'D','') as int)) < day(#d) then 1 else 0 end + case when sign(cast(replace(p1,'D','') as int)) = -1 then -1 else 0 end, 0))
when 'W' then dateadd(week,datediff(week,0,dateadd(week,abs(cast(replace(p1,'W','') as int))-1,datefromparts(year(#d) + case when abs(cast(replace(p1,'W','') as int)) <= datepart(week,#d) then 1 else 0 end + case when sign(cast(replace(p1,'W','') as int)) = -1 then -1 else 0 end,1,1))), 0)
when 'M' then datefromparts(year(#d) + case when abs(cast(replace(p1,'M','') as int)) <= month(#d) then 1 else 0 end + case when sign(cast(replace(p1,'M','') as int)) = -1 then -1 else 0 end,abs(cast(replace(p1,'M','') as int)),1)
when 'Q' then datefromparts(year(#d) + case when abs(cast(replace(p1,'Q','') as int)) <= datepart(quarter,#d) then 1 else 0 end + case when sign(cast(replace(p1,'Q','') as int)) = -1 then -1 else 0 end,((abs(cast(replace(p1,'Q','') as int))-1)*3)+1,1)
when 'Y' then datefromparts(abs(cast(replace(p1,'Y','') as int)),1,1)
end
end
else ''
end
)
) as v1(retp1)
outer apply(values(case right(p2,1)
when 'D' then dateadd(day,cast(replace(replace(p2,'W',''),'D','') as int),retp1)
when 'W' then dateadd(day,cast(replace(p2,'W','') as int) * 7,retp1)
when 'M' then dateadd(month,cast(replace(p2,'M','') as int),retp1)
when 'Q' then dateadd(quarter,cast(replace(p2,'Q','') as int),retp1)
when 'Y' then dateadd(year,cast(replace(p2,'Y','') as int),retp1)
else retp1
end
)
) as v2(retp2)
outer apply(values(case right(p3,1)
when 'D' then dateadd(day,cast(replace(replace(p3,'W',''),'D','') as int),retp2)
when 'W' then dateadd(day,cast(replace(p3,'W','') as int) * 7,retp2)
when 'M' then dateadd(month,cast(replace(p3,'M','') as int),retp2)
when 'Q' then dateadd(quarter,cast(replace(p3,'Q','') as int),retp2)
when 'Y' then dateadd(year,cast(replace(p3,'Y','') as int),retp2)
else retp2
end
)
) as v3(retp3);
Output
GivenDate
Expression
ExpectedValue
ReturnedValue
ValueCheck
1996-05-21
-CW+1W+1D
1996-05-28
1996-05-28
Match
1996-05-21
CQ+1M-10D
1996-07-20
1996-07-20
Match
1996-05-21
+CW+1D
1996-05-28
1996-05-28
Match
1996-05-21
+CW+1WD
1996-05-28
1996-05-28
Match
1996-05-21
CM+30D
1996-06-30
1996-06-30
Match
1996-05-21
-WD2
1996-05-14
1996-05-14
Match
1996-05-21
WD3
1996-05-22
1996-05-22
Match
1996-05-21
-D3
1996-05-03
1996-05-03
Match
1996-05-21
-D27
1996-04-27
1996-04-27
Match
1996-05-21
D3
1996-06-03
1996-06-03
Match
1996-05-21
D27
1996-05-27
1996-05-27
Match
1996-05-21
10D
1996-05-31
1996-05-31
Match
1996-05-21
1W
1996-05-28
1996-05-28
Match
1996-05-21
W2
1997-01-06
1997-01-06
Match
1996-05-21
-W2
1996-01-08
1996-01-08
Match
1996-05-21
M12
1996-12-01
1996-12-01
Match
1996-05-21
M2
1997-02-01
1997-02-01
Match
1996-05-21
-M2
1996-02-01
1996-02-01
Match
1996-05-21
Q4
1996-10-01
1996-10-01
Match
1996-05-21
Q2
1997-04-01
1997-04-01
Match
1996-05-21
-Q2
1996-04-01
1996-04-01
Match
Can anyone explain me why these two statements returns different results?
SELECT CASE WHEN NOT((NULL = NULL) OR (1 != 1)) THEN 1 ELSE 0 END
SELECT CASE WHEN NOT((NULL = NULL) AND (1 != 1)) THEN 1 ELSE 0 END
I know that NULL compared with anything gives false and I wanted to use that property but I stopped at commands similar to above. My real statements instead of NULLs use variables that can be NULL but I simplified them to show where is the problem. I thought that it has something with operation order but it seems that's not it.
I know that NULL compared with anything gives false
This isn't correct, NULL compared with anything evaluates to unknown, not false, a quick example:
SELECT CASE WHEN (NULL = NULL) THEN 'True'
WHEN NOT(NULL = NULL) THEN 'False'
ELSE 'Other'
END
Will give the third option of Other.
If we rewrite your logic (still the same meaning, but it becomes more clear):
SELECT CASE WHEN (NULL <> NULL) AND (1 = 1) THEN 1 ELSE 0 END
SELECT CASE WHEN (NULL <> NULL) OR (1 = 1) THEN 1 ELSE 0 END
So in the first instance you have WHEN [Unknown] AND [True] which is false, but in the second you have WHEN [Unknown] OR [True] which is true, so returns 1.
If you rewrite the query with variables, then inspect the execution plan XML, you can see that SQL Server rewrites the expression as above during compilation:
DECLARE #a INT = NULL, #b INT = NULL, #c INT = 1, #d INT = 1;
SELECT TOP 1
CASE WHEN NOT((#a = #b) OR (#c != #d)) THEN 1 ELSE 0 END,
CASE WHEN NOT((#a = #b) AND (#c != #d)) THEN 1 ELSE 0 END
-- first query
SELECT CASE WHEN NOT((NULL = NULL) AND (1 != 1)) THEN 1 ELSE 0 END
=
SELECT CASE WHEN NOT(unknown AND false) THEN 1 ELSE 0 END
=
SELECT CASE WHEN NOT(false) THEN 1 ELSE 0 END
=
SELECT CASE WHEN true THEN 1 ELSE 0 END
=
1
-- second query
SELECT CASE WHEN NOT((NULL = NULL) OR (1 != 1)) THEN 1 ELSE 0 END
=
SELECT CASE WHEN NOT(unknown OR false) THEN 1 ELSE 0 END
=
SELECT CASE WHEN NOT(unknown) THEN 1 ELSE 0 END
=
SELECT CASE WHEN unknown THEN 1 ELSE 0 END
=
else matched, so 0
And to D0dger's question from comments:
It's more interesting why SELECT CASE WHEN (NULL = NULL) OR (1 != 1) THEN 1 ELSE 0 END and SELECT CASE WHEN NOT((NULL = NULL) OR (1 != 1)) THEN 1 ELSE 0 END returns 0
SELECT CASE WHEN (NULL = NULL) OR (1 != 1) THEN 1 ELSE 0 END
=
SELECT CASE WHEN unknown OR false THEN 1 ELSE 0 END
=
SELECT CASE WHEN unknown THEN 1 ELSE 0 END
=
else matched, so 0
OR (Transact-SQL), AND (Transact-SQL)
So, there are 2 options: Either you have ANSI_NULLS ON (and you should) or you have ANSI_NULLS OFF.
In the first case, any comparison with NULL returns NULL (even comparisons between NULL values such as yours).
In the second case, sql server will evaluate comparisons between NULL values (e.g. NULL=NULL will return true).
So before considering the different results in your queries you must first consider that comparing NULL with anything, doesn't evaluate to false but NULL
How I can remove only the first 0 character from a varchar?
For example '000303' ==> '00303'
I tried this without success, all the 0 characters are removed:
SELECT SUBSTRING('000303', PATINDEX('%[^0]%', '000303'),LEN('000303'));
Thanks
Try using STUFF Function
SELECT CASE WHEN LEFT('000303',1) = '0' THEN STUFF('000303',1,1,'') ELSE '000303' END
or Use RIGHT Function
SELECT CASE WHEN LEFT('000303',1) = '0' THEN RIGHT('000303', LEN('000303')-1) ELSE '000303' END
Instead of LEFT('000303',1) = '0' check you can also use
charindex('0','000303') = 1 or
'000303' LIKE '0%' (ughai suggestion)
Try this:
SELECT RIGHT(MyColumn, LEN(MyColumn) - 1)
This will remove the first character from the varchar column.
If it is specific to 0 then try this:
SELECT CASE WHEN LEFT(MyColumn,1) = '0'
THEN RIGHT(MyColumn, LEN(MyColumn) - 1)
ELSE
MyColumn END
Something like
SELECT CASE WHEN LEFT(Mycol,1) = '0' THEN SUBSTRING(MyCOL, 2, LEN(MyCOL)) END
This question already has answers here:
Closed 10 years ago.
Possible Duplicate:
CASE equivalent of a nested IIF statement
I'm trying to convert the following Access query into T-SQL, but I'm having problems with IIF statement.
ACCESS Query
SELECT
qry_LAB_LOAD_Prequery2.Asset AS SITE_REF,
Format([Det],"00000") AS DET_,
IIf(IsNull([MT]),"0",IIf(Right([SP_Ref],2)="WZ"
And IsNull([LabLoadFileSuffix_MT])
And [MT]>0,CStr([MT]) & "CT",CStr([MT]) & [LabLoadFileSuffix_MT])) AS MT_COUNT,
IIf(IIf(IsNull([MT]),0,[MT])>=IIf(IsNull([IM]),0,[IM]),0,
IIf(IsNull([IM]),"0",IIf(Right([SP_Ref],2)="WZ"
And IsNull([LabLoadFileSuffix_IM]),CStr([IM])
& "CT",CStr([IM]) & [LabLoadFileSuffix_IM]))) AS IM_COUNT,
qry_LAB_LOAD_Prequery2.[2012 Sample Point] AS SP_REF, qry_LAB_LOAD_Prequery2.Area
FROM qry_LAB_LOAD_Prequery2
WHERE (((IIf(IsNull([IM]),0,[IM])+IIf(IsNull([MT]),0,[MT]))>0));
I've tried to convert a part of it. Can anyone help me correct the nested IID statement as I'm not sure.
SQL Query
SELECT qry_LAB_LOAD_Prequery2.Asset AS SITE_REF,
RIGHT('00000' + CAST([Det] AS VARCHAR(5)),5) AS DET_,
----- (the nested iff statements)
qry_LAB_LOAD_Prequery2.[2012 Sample Point] AS SP_REF,
qry_LAB_LOAD_Prequery2.Area
FROM qry_LAB_LOAD_Prequery2
where (ISNull([IM],0) + ISNULL([MT],0)) > 0
To convert the nested IIF you need user CASE.
Lets take the first one:
Reformating I get:
IIf(
IsNull([MT]),
"0",
IIf(Right([SP_Ref],2)="WZ"
And IsNull([LabLoadFileSuffix_MT])
And [MT]>0,
CStr([MT]) & "CT",
CStr([MT]) & [LabLoadFileSuffix_MT]
)
) AS MT_COUNT,
It Converts to :
CASE
WHEN [MT] IS NULL
THEN "0",
ELSE
CASE
WHEN Right([SP_Ref],2) = "WZ"
AND ([LabLoadFileSuffix_MT] IS NULL
AND [MT]>0
THEN
CAST([MT] AS VARCHAR) + "CT"
ELSE
CAST([MT] AS VARCHAR) + [LabLoadFileSuffix_MT]
END
END AS MT_COUNT,
Which simplifies to :
CASE
WHEN [MT] IS NULL
THEN "0",
ELSE
CAST([MT] AS VARCHAR)
+ CASE
WHEN Right([SP_Ref],2) = "WZ"
AND ([LabLoadFileSuffix_MT] IS NULL
AND [MT]>0
THEN
"CT"
ELSE
[LabLoadFileSuffix_MT]
END
END AS MT_COUNT,
For tee second one I reformatted and then converted it to the following
CASE WHEN
CASE
WHEN [MT] IS NULL THEN 0
ELSE [MT]
END
>=
CASE
WHEN [IM] IS NULL THEN 0
ELSE [IM]
END ,
THEN 0,
ELSE
CASE
WHEN [IM] IS NULL THEN "0"
ELSE
CASE
WHEN Right([SP_Ref],2) = "WZ" AND [LabLoadFileSuffix_IM] IS NULL THEN CAST([IM] AS VARCHAR) + "CT"
ELSE CAST([IM] AS VARCHAR) + [LabLoadFileSuffix_IM]
END
END
END
AS IM_COUNT,
Whick reduces to
CASE
WHEN ISNULL([MT], 0) > = ISNULL([IM], 0)
THEN 0,
ELSE
CASE
WHEN [IM] IS NULL THEN "0"
ELSE
CAST([IM] AS VARCHAR) +
CASE
WHEN Right([SP_Ref],2) = "WZ" AND [LabLoadFileSuffix_IM] IS NULL THEN "CT"
ELSE [LabLoadFileSuffix_IM]
END
END
END
AS IM_COUNT,
Which essentially boils down to 2 very similar expressions for both columns.
Try a nested case statement.
So just taking the first 2 of your lines:
IIf(IsNull([MT]),"0",IIf(Right([SP_Ref],2)="WZ"
And IsNull([LabLoadFileSuffix_MT])
would become
CASE WHEN MT IS NULL
THEN 0
ELSE
CASE WHEN RIGHT(SP_Ref, 2) = "WZ" AND LabLoadFileSuffix_MT IS NULL
THEN
...
ELSE ...
END
END