How to write Navision CALCDATE function in TSQL - sql-server
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
Related
PostgreSQL JSON building an array without null values
The following query SELECT jsonb_build_array(jsonb_build_object('use', 'Home'), CASE WHEN 1 = 2 THEN jsonb_build_object('use', 'Work') END) produces [{"use":"Home"},null] When I actually want [{"use":"Home"}] How do I go about doing this? json_strip_nulls() does not work for me.
By using a PostgreSQL array like that: SELECT array_to_json(array_remove(ARRAY[jsonb_build_object('use', 'Home'), CASE WHEN 1 = 2 THEN jsonb_build_object('use', 'Work') END], null)) which does produce: [{"use": "Home"}] while, to be sure: SELECT array_to_json(array_remove(ARRAY[jsonb_build_object('use', 'Home'), CASE WHEN 1 = 2 THEN jsonb_build_object('use', 'Work') END, jsonb_build_object('real_use', 'NotHome')], null)) does produce: [{"use": "Home"},{"real_use": "NotHome"}]
Creating a custom function seems to be the simplest way. create or replace function jsonb_build_array_without_nulls(variadic anyarray) returns jsonb language sql immutable as $$ select jsonb_agg(elem) from unnest($1) as elem where elem is not null $$; select jsonb_build_array_without_nulls( jsonb_build_object('use', 'home'), case when 1 = 2 then jsonb_build_object('use', 'work') end ) jsonb_build_array_without_nulls --------------------------------- [{"use": "home"}] (1 row)
I'm assuming this query is generaetd dynamically, somehow. If you're in control of the SQL generation, you can also use ARRAY_AGG(...) FILTER(...) instead, which, depending on your real world query, might be more convenient than using all the different array conversion functions suggested by Patrick. SELECT ( SELECT json_agg(v) FILTER (WHERE v IS NOT NULL) FROM ( VALUES (jsonb_build_object('use', 'Home')), (CASE WHEN 1 = 2 THEN jsonb_build_object('use', 'Work') END) ) t (v) ) Or also: SELECT ( SELECT json_agg(v) FROM ( VALUES (jsonb_build_object('use', 'Home')), (CASE WHEN 1 = 2 THEN jsonb_build_object('use', 'Work') END) ) t (v) WHERE v IS NOT NULL )
One other way that this can be handled is the following: SELECT jsonb_build_array( jsonb_build_object('use', 'Home'), CASE WHEN 1 = 2 THEN jsonb_build_object('use', 'Work') ELSE '"null"' END ) - 'null' (Unfortunately it's not really possible to do much with null by itself in postgres -- or most other DBs) In the case above, '"null"' could be replaced with just about any unique string that would not be mistaken for live data in the array. I would not use numbers since - 0 would actually try to remove the first item from the array, rather than a number within the array. But you could probably use '"0"' and remove using something like - '0' if you wanted. For those not using CASE, a COALESCE could be used to convert nulls into the desired string (alas, no NVL, IFNULL or ISNULL in postgres, but at least COALESCE is portable)
SQL Server 2012 and NULL comparison
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
Optional Conditional statement in where clause
I need to add a condition in my WHERE clause that will include items in a search if #UserRoleId = 4, else exclude them from the search (SQL Server). INSERT INTO #partData ( PartPrefix, PartNumber, MyProDescription, ItemNumber, PartType, Side, VehicleLocation, MyProPrice ) SELECT CASE WHEN LEFT(myParts.MyProDescription, 4) = 'FOIL' THEN 'FOIL' ELSE LEFT(myParts.PartNumber, 3) END, PartNumber, MyProDescription, ItemNumber, PartType, Side, VehicleLocation, MyProPrice FROM MyProExport myParts WHERE #year BETWEEN myParts.YearFrom AND myParts.YearTo AND myParts.CarMake = #make AND REPLACE(myParts.CarModel, ' ', '') = REPLACE(#model, ' ', '') AND ( myParts.EngineDisplacement = #engine OR myParts.EngineDisplacement = 'ALL' ) AND LEFT(myParts.PartNumber, 3) NOT IN ('300','400') -- Exclude these parts if not #UserRoleId = 4 The last statement needs to be modified to display 300 and 400 only if #UserRoleId is = 4. Else just show 300. I’m confused on how to set this up. I’ve tried: CASE WHEN #UserRoleId = 4 THEN LEFT(myParts.Number, 3) NOT IN ('300','400') -- Exclude parts with no luck...
Instead of a case statement, just use AND/OR logic. AND (LEFT(myParts.HollanderNumber, 3) = '300' OR (#UserRoleID = 4 AND LEFT(myParts.HollanderNumber, 3) = '400')) This will always display 300 and only display 400 if the #UserRoleID = 4
How do I put a CASE statement in a SUBSTRING statement?
The purpose of this that I'm trying to compare the usernames of two email addresses and see if they're the same. Really, all I want is for this to work. When I run the query, all I get is: Invalid length parameter passed to the LEFT or SUBSTRING function. Note: I changed the query. This better illustrates what I'm trying to do. Note2: I've made the changes so it works properly, but now if the parameter is '', then I get: Invalid length parameter passed to the LEFT or SUBSTRING function. declare #ReportParameter1 nvarchar(16) set #ReportParameter1 = 'manmoon#test1.com' declare #ReportParameter2 nvarchar(16) set #ReportParameter2 = '' select 'test' where SUBSTRING (case #ReportParameter1 when '' then 'x#' else #ReportParameter1 end, 1, Charindex('#', case #ReportParameter1 when '' then 'x#' else #ReportParameter1 end) - 1) = SUBSTRING (case #ReportParameter2 when '' then 'x#' else #ReportParameter2 end, 1, Charindex('#', case #ReportParameter2 when '' then 'x#' else #ReportParameter2 end) - 1) Here's the where clause I used to fix the problem. However, this will teach me to be more careful when copying and pasting. WHERE (substring(#ReportParameter1, 1, case when (CHARINDEX('#', #ReportParameter1) - 1) < 1 then 1 else CHARINDEX('#', #ReportParameter1) - 1 end) = SUBSTRING(#ReportParameter2, 1, CHARINDEX('#', #ReportParameter2) - 1))
The error comes from not comparing the result of the substring to anything. A string is not a boolean expression. Edit: Now that you edited your question, that answer doesn't make any sense any more. I tested your substring expressions with empty strings, and I don't get any error. If you on the other hand have strings that are not empty, but doesn't contain any # character, then you get the error that you describe. To handle that you could do like this: ... where case when #ReportParameter1 = '' or charindex('#', #ReportParameter1) = 0 then 'x' else substring(#ReportParameter1, 1, charindex('#', #ReportParameter1) - 1) end = case when #ReportParameter2 = '' or charindex('#', #ReportParameter2) = 0 then 'x' else substring(#ReportParameter2, 1, charindex('#', #ReportParameter2) - 1) end Note however that two strings that are not email addresses would compare as equal, as 'x' = 'x', so you might want to use different fallback values in the expressions... Edit 2: Come to think of it, you don't need to check for empty strings if you check for the # character, as an empty string can't contain a # character: ... where case when charindex('#', #ReportParameter1) = 0 then 'x' else substring(#ReportParameter1, 1, charindex('#', #ReportParameter1) - 1) end = case when charindex('#', #ReportParameter2) = 0 then 'x' else substring(#ReportParameter2, 1, charindex('#', #ReportParameter2) - 1) end
put the substring in the case... you basically want a static value on one case... on the other ... use the substring on the "else" case GPS_Quotes.[Sales Engineer] when '' then 'some constant value' else substring(GPS_Quotes.[Sales Engineer], 1, ...{I don't understand what your are trying to do}) end
How to get all values including null in a SQL Server case statement?
I have a big stored procedure, and basically I want to select all values (including null) if my variable #DimBrowserId is set to 0. I am using a case statement, however this is only catching values that actually have something and ignoring the NULL valued fields. Because I am using the = clause in the WHERE I cannot do IS NULL. I do not want to have to write 2 IF statements because the stored procedure would then be enormous, so I want to know how to get null values as well. Here is my code: SELECT DATEPART(yy, DATEADD(mi, #Mdelta, d.DimDateValue)), DisableCount = COUNT(*) FROM dbo.FactDisable AS f JOIN dbo.DimDate AS d ON f.DimDateId = d.DimDateId JOIN dbo.DimDevice AS v ON f.DimDeviceId = v.DimDeviceId WHERE d.DimDateValue >= #StartDateGMT AND d.DimDateValue <= #EndDateGMT AND f.IsTest = #IncludeTest AND f.DimProductId = #DimProductId AND v.DimBrowserId = CASE WHEN #DimBrowserId = 0 THEN v.DimBrowserId ELSE #DimBrowserId END GROUP BY DATEPART(yy, DATEADD(mi, #Mdelta, d.DimDateValue)) The code is near the CASE clause. Thanks
Change that line to be AND (#DimBrowserID = 0 OR #DimBrowserID = v.DimBrowserId) If #DimBroserID is 0 then no filtering will be applied for this line.
Use ISNULL: SELECT DATEPART(yy,DATEADD(mi,#Mdelta,d.DimDateValue)), DisableCount=COUNT(*) FROM dbo.FactDisable AS f JOIN dbo.DimDate AS d ON f.DimDateId = d.DimDateId JOIN dbo.DimDevice AS v ON f.DimDeviceId = v.DimDeviceId WHERE d.DimDateValue >= #StartDateGMT AND d.DimDateValue <= #EndDateGMT AND f.IsTest = #IncludeTest AND f.DimProductId = #DimProductId AND v.DimBrowserId = CASE WHEN ISNULL(#DimBrowserId,0) = 0 THEN v.DimBrowserId ELSE #DimBrowserId END GROUP BY DATEPART(yy,DATEADD(mi,#Mdelta,d.DimDateValue))
CASE WHEN COALESCE(#MightBeNull, 0) = 0 THEN ZeroResult ... will be treated as zero if #MightBeNull is null, and whatever #MightBeNull is if it's not null.
Assuming null means any browser, a better data model for this scenario might be to set an ID that identifies any browser, instead of setting it to null. You probably know what you are running into is NULL does not equal NULL in a comparison. Assuming you don't have control of the data model to fix that, one option would be to coalesce your NULL values to an unused id. The resulting WHERE clause would look like this, assuming -1 is the unused value you choose. AND COALESCE(v.DimBrowserId, -1) = CASE WHEN #DimBrowserId = 0 THEN COALESCE(v.DimBrowserId, -1) ELSE #DimBrowserId END