如何在 TSQL 中编写 Navision CALCDATE 函数 SQL输出

问题描述

以下是 Navision CALCDATE function 的定义:

NewDate := CALCDATE(DateExpression [,Date])

需要两个参数:

  • DateExpression 在 Navision 中的类型为 DateFormula,在 sql 中存储为 varchar(32)
  • Date,在 sql 中表示日期时间。为了简化问题,我假设它不是可选的(在 Navsion 中是可选的)

我需要从 sql 访问 Navision 数据并根据存储在那里的 DateFormulas 进行一些计算。 我知道如何在 sql CLR 中编写此类函数,因此请不要在您的答案中包含此内容

  1. 如何在 Tsql 中编写 CALCDATE 函数
  2. [可选] 是否可以仅在 sql 查询中使用一些(辅助)CTE(这将允许我不向 Navsion 数据库添加函数,因此不修改它)?
  3. sql CLR 是我唯一的选择吗?

我对这个功能的主要问题是它似乎必须按以下步骤完成:

  1. 解析日期表达式
  2. 将解析结果应用于日期参数并生成新日期。

但是解析部分怎么做呢?
例如 DateExpression = '-CW+1W+1D' 意味着我们必须按照这个顺序对我们的 Date 参数做 3 件事:

  1. 查找上周一(本周开始)'-CW'
  2. 添加 1 周(7 天)“+1W”
  3. 添加 1 天“+1D”

它可以写成更短的形式“+CW+1D”,这意味着下周一和 1 天。 通常这个 varchar(32) 可以包含 1 个或多个这样的表达式,并且它们必须按提供的顺序应用于 Date 参数。

如何在sql中解析这种varchar(32)?
在 C# 中,我会在循环中或递归中执行此操作,但是如何在 sql(集合)中执行此操作?
如何将这种结构化的 varchar(32) 转换为一组公式。 也许我需要以某种方式将它转换为 XML 并使用 sql Server XML 函数查询它 - 但如何?


编辑: 我想如何使用它?

我想将此函数用作更大查询的一部分。
例如(简化):

    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

我的意思是,那个级别。[宽限期]在表格中,可以由用户编辑并且可以随时更改。所以我不能走任何捷径并预先计算几个值。

解决方法

这个答案在这里主要是作为一个例子,说明在纯 SQL 中尝试和实现是多么愚蠢(也因为我认为这是一个有趣的挑战)。

它不打算用于任何类型的生产能力,我什至不确定它是否正确实现了 calcdate 的功能或是否适用于 calcdate 允许的所有可能场景。

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',('+CW+1WD',('CM+30D','19960630'),('-WD2','19960514'),('WD3','19960522'),('-D3','19960503'),('-D27','19960427'),('D3','19960603'),('D27','19960527'),('10D','19960531'),('1W',('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,''
                ) + '|||' as Fmt
    from @t
),p as
(
    select Expression,Fmt,left(v.Fmt,charindex('|',v.Fmt,0)-1) as p1,substring(v.Fmt,0)+1,(charindex('|',0)+1)-1) - (charindex('|',0))
                      ) as p2,replace(substring(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,@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,eomonth(@d,-1)) end
                                       when 'Q' then dateadd(day,case when left(p1,1) = '+' then -1 else 0 end,dateadd(quarter,datediff(quarter,0))
                                       when 'Y' then dateadd(year,datediff(year,0)
                                     else ''
                                     end
                            when isnumeric(substring(p1,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,'D',@d)
                                                 when 'W' then dateadd(week,'W',@d)
                                                 when 'M' then dateadd(month,'M',@d)
                                                 when 'Q' then dateadd(quarter,'Q',@d)
                                                 when 'Y' then dateadd(year,'Y',@d)
                                                 end
                                          end
                            when isnumeric(substring(p1,1)) = 0       -- <Unit><Number>
                                then case when substring(p1,right(p1,1)-1,dateadd(week,@d) - case when left(p1,1) = '-' then 1 else 0 end,0))
                                          else case substring(p1,abs(cast(replace(p1,'') as int))-1,dateadd(month,datediff(month,@d) + case when abs(cast(replace(p1,'') as int)) < day(@d) then 1 else 0 end + case when sign(cast(replace(p1,'') as int)) = -1 then -1 else 0 end,0))
                                                 when 'W' then dateadd(week,datefromparts(year(@d) + case when abs(cast(replace(p1,'') as int)) <= datepart(week,@d) then 1 else 0 end + case when sign(cast(replace(p1,1))),0)
                                                 when 'M' then datefromparts(year(@d) + case when abs(cast(replace(p1,'') as int)) <= month(@d) then 1 else 0 end + case when sign(cast(replace(p1,'') as int)),1)
                                                 when 'Q' then datefromparts(year(@d) + case when abs(cast(replace(p1,'') as int)) <= datepart(quarter,((abs(cast(replace(p1,'') as int))-1)*3)+1,1)
                                                 when 'Y' then datefromparts(abs(cast(replace(p1,1)
                                                 end
                                          end
                            else ''
                            end
                          )
                   ) as v1(retp1)
        outer apply(values(case right(p2,1)
                           when 'D' then dateadd(day,cast(replace(replace(p2,''),retp1)
                           when 'W' then dateadd(day,cast(replace(p2,'') as int) * 7,retp1)
                           when 'M' then dateadd(month,retp1)
                           when 'Q' then dateadd(quarter,retp1)
                           when 'Y' then dateadd(year,retp1)
                           else retp1
                           end
                          )
                   ) as v2(retp2)
        outer apply(values(case right(p3,1)
                            when 'D' then dateadd(day,cast(replace(replace(p3,retp2)
                            when 'W' then dateadd(day,cast(replace(p3,retp2)
                            when 'M' then dateadd(month,retp2)
                            when 'Q' then dateadd(quarter,retp2)
                            when 'Y' then dateadd(year,retp2)
                            else retp2
                            end
                          )
                   ) as v3(retp3);

输出

给定日期 表达 预期价值 返回值 值检查
1996-05-21 -CW+1W+1D 1996-05-28 1996-05-28 匹配
1996-05-21 CQ+1M-10D 1996-07-20 1996-07-20 匹配
1996-05-21 +CW+1D 1996-05-28 1996-05-28 匹配
1996-05-21 +CW+1WD 1996-05-28 1996-05-28 匹配
1996-05-21 CM+30D 1996-06-30 1996-06-30 匹配
1996-05-21 -WD2 1996-05-14 1996-05-14 匹配
1996-05-21 WD3 1996-05-22 1996-05-22 匹配
1996-05-21 -D3 1996-05-03 1996-05-03 匹配
1996-05-21 -D27 1996-04-27 1996-04-27 匹配
1996-05-21 D3 1996-06-03 1996-06-03 匹配
1996-05-21 D27 1996-05-27 1996-05-27 匹配
1996-05-21 10D 1996-05-31 1996-05-31 匹配
1996-05-21 1W 1996-05-28 1996-05-28 匹配
1996-05-21 W2 1997-01-06 1997-01-06 匹配
1996-05-21 -W2 1996-01-08 1996-01-08 匹配
1996-05-21 M12 1996-12-01 1996-12-01 匹配
1996-05-21 M2 1997-02-01 1997-02-01 匹配
1996-05-21 -M2 1996-02-01 1996-02-01 匹配
1996-05-21 第四季度 1996-10-01 1996-10-01 匹配
1996-05-21 第二季度 1997-04-01 1997-04-01 匹配
1996-05-21 -Q2 1996-04-01 1996-04-01 匹配