T-SQL 计算日期的重复次数

T-SQL Calculate recurrences of a Date

我正在研究计算一周重复的 CTE,但是当 模式 跨年时我遇到了一些问题。

CTE 应该 根据以下参数计算所有出现次数:

这是我存储模式的方式:

/*
    Pattern Table
*/
CREATE TABLE Pattern (
    [Id] [int] IDENTITY(1,1) NOT NULL PRIMARY KEY
,   [Subject] [nvarchar](100) NULL
,   [RecurrenceCount] [int] NULL
,   [WeekDays] [varchar](max) NULL
,   [StartDate] [datetime] NULL
,   [EndDate] [datetime] NULL
,   [Periodicity] [int] NULL
,   [StartingWeek] [int] NULL

);

下面是我用来测试 CTE 的几个模式:

/*
    Pattern samples for test
*/
Insert into Pattern Values (N'Every 5 Weeks Fri, Sat, Sun', 72, 'Friday, Saturday, Sunday', N'2016-12-02', N'2016-12-02', 5, datepart(wk, N'2016-12-02'));
Insert into Pattern Values (N'Every 3 Weeks Tue, Wed, Thu', 20, 'Tuesday, Wednesday, Thursday', N'2016-11-01', N'2016-11-01', 3, datepart(wk, N'2016-11-01'));

考虑到一周的第一天,我开始计算 星期一

SET DATEFIRST 1

这是我用来 运行 这个计算的 CTE:

/*
    Display Patterns
*/
select * from Pattern

DECLARE @mindate DATE       = (SELECT MIN(StartDate) FROM Pattern)
DECLARE @maxmindate DATE    = (SELECT MAX(StartDate) FROM Pattern)
DECLARE @maxcount INT       = (SELECT MAX(RecurrenceCount) FROM Pattern)
DECLARE @maxdate DATE       = DATEADD(WK, @maxcount + 10, @maxmindate)

/*
    CTE to generate required occurrences
*/
;With cteKeyDate As (
    Select 
        KeyStartDate = @MinDate, 
        KeyDOW = DateName(WEEKDAY, @MinDate), 
        KeyWeek = datepart(WK,@MinDate)
    Union All
    Select 
        KeyStartDate =  DateAdd(DD, 1, df.KeyStartDate) ,
        KeyDOW = DateName(WEEKDAY,DateAdd(DD, 1, df.KeyStartDate)), 
        KeyWeek= DatePart(WK,DateAdd(DD, 1, df.KeyStartDate))
    From cteKeyDate DF
    Where DF.KeyStartDate <= @MaxDate
) 

SELECT 
    Id, KeyStartDate, KeyDow, KeyWeek, RowNr, OccNr = ROW_NUMBER() OVER (PARTITION BY Id ORDER BY StartDate) 
FROM
    (Select 
        A.Id
        ,A.StartDate
        ,A.EndDate
        ,Count = A.RecurrenceCount
        ,Days = A.WeekDays
        ,Every = A.Periodicity              
        ,KeyStartDate = CASE
        /* 
            if no periodicity (1) then it is sequential
            if periodicity, first week doesn't apply (MIN KeyWeek)
        */
        WHEN A.Periodicity = 1 
            OR ( Periodicity <> 1 AND (SELECT MIN(C.StartingWeek) FROM Pattern AS C WHERE C.Id = A.Id) = KeyWeek )
            THEN KeyStartDate
        /* Otherwise formula ADD WEEKS => Current Week Min Week */
        ELSE
            DATEADD( WK, ((A.Periodicity - 1) * ( KeyWeek - (SELECT MIN(C.StartingWeek) FROM Pattern AS C WHERE C.Id = A.Id) ) ) , KeyStartDate )
        END
        ,KeyDow
        ,KeyWeek
        ,RowNr = Row_Number() over (Partition By A.Id Order By B.KeyStartDate)
        ,Periodicity = A.Periodicity
    from 
        Pattern A
        Join cteKeyDate B on B.KeyStartDate >= DATEADD(DAY, -1, A.StartDate) and Charindex(KeyDOW, A.WeekDays) > 0
    ) Final                 
Where 
    RowNr <= Count AND Id = 1
Option (maxrecursion 32767)

现在,如果我再次测试我的模式,例如,第一个,我得到这个结果,当明年发生时,它有错误。 RowNr 15 是错误的,因为它应该发生在 4 月 23 日(星期日)而不是下周。

Id  KeyStartDate    KeyDow      KeyWeek RowNr   OccNr
1   02.12.2016      Friday      49      1       1
2   03.12.2016      Saturday    49      2       2
3   04.12.2016      Sunday      49      3       3
4   06.01.2017      Friday      50      4       4
5   07.01.2017      Saturday    50      5       5
6   08.01.2017      Sunday      50      6       6
7   10.02.2017      Friday      51      7       7
8   11.02.2017      Saturday    51      8       8
9   12.02.2017      Sunday      51      9       9
10  17.03.2017      Friday      52      10      10
11  18.03.2017      Saturday    52      11      11
12  19.03.2017      Sunday      52      12      12
13  21.04.2017      Friday      53      13      13
14  22.04.2017      Saturday    53      14      14
15  28.04.2013      Sunday      1       15      15
16  31.05.2013      Friday      2       16      16
17  01.06.2013      Saturday    2       17      17

而第二个模式计算得很好。我认为当模式跨越一年并且周数在 SQL 中重置为 0 时我的逻辑有问题但是我找不到解决方案,我现在挣扎了几天。

您可以使用示例执行代码 here

您遇到该问题的原因是您正在使用日期部分获取周数,因此一旦年份更改,日期部分将返回到 1。这应该更改... 我做了这个更改,日期现在按顺序排列,检查一下。 顺便说一句,更改是第一个 CTE。

/*
    Display Patterns
*/
select * from Pattern

DECLARE @mindate DATE       = (SELECT MIN(StartDate) FROM Pattern)
DECLARE @maxmindate DATE    = (SELECT MAX(StartDate) FROM Pattern)
DECLARE @maxcount INT       = (SELECT MAX(RecurrenceCount) FROM Pattern)
DECLARE @maxdate DATE       = DATEADD(WK, @maxcount + 10, @maxmindate)

declare @minWeekPart INT = DATEPART(WK,@MinDate)
/*
    CTE to generate required occurrences
*/
;With cteKeyDate As (
    Select 
        KeyStartDate = @MinDate, 
        KeyDOW = DateName(WEEKDAY, @MinDate), 
        KeyWeek = @minWeekPart
    Union All
    Select 
        KeyStartDate =  DateAdd(DD, 1, df.KeyStartDate) ,
        KeyDOW = DateName(WEEKDAY,DateAdd(DD, 1, df.KeyStartDate)), 
        KeyWeek= @minWeekPart + datediff(WK,@MinDate,DateAdd(DD, 1, df.KeyStartDate))
    From cteKeyDate DF
    Where DF.KeyStartDate <= @MaxDate
) 
--select * from cteKeyDate
--    order by 1
--Option (maxrecursion 32767)


SELECT 
    Id, KeyStartDate, KeyDow, KeyWeek, RowNr, OccNr = ROW_NUMBER() OVER (PARTITION BY Id ORDER BY StartDate) 
FROM
    (Select 
        A.Id
        ,A.StartDate
        ,A.EndDate
        ,Count = A.RecurrenceCount
        ,Days = A.WeekDays
        ,Every = A.Periodicity              
        ,KeyStartDate = CASE
        /* 
            if no periodicity (1) then it is sequential
            if periodicity, first week doesn't apply (MIN KeyWeek)
        */
        WHEN A.Periodicity = 1 
            OR ( Periodicity <> 1 AND (SELECT MIN(C.StartingWeek) FROM Pattern AS C WHERE C.Id = A.Id) = KeyWeek )
            THEN KeyStartDate
        /* Otherwise formula ADD WEEKS => Current Week Min Week */
        ELSE
            DATEADD( WK, ((A.Periodicity - 1) * ( KeyWeek - (SELECT MIN(C.StartingWeek) FROM Pattern AS C WHERE C.Id = A.Id) ) ) , KeyStartDate )
        END
        ,KeyDow
        ,KeyWeek
        ,RowNr = Row_Number() over (Partition By A.Id Order By B.KeyStartDate)
        ,Periodicity = A.Periodicity
    from 
        Pattern A
        Join cteKeyDate B on B.KeyStartDate >= DATEADD(DAY, -1, A.StartDate) and Charindex(KeyDOW, A.WeekDays) > 0
    ) Final                 
Where 
    RowNr <= Count AND Id = 1
    order by 2 
Option (maxrecursion 32767)

我在 rextester 中做了 运行,你可以在这里找到它 --> http://rextester.com/GWEY37271

花点时间在这上面。您使用的计算有缺陷。除非您的问题中没有指出特殊规则,否则我不明白为什么有些日期很特别。我更喜欢使用变量 tables.

    /*
        Pattern Table
    */
    DECLARE @Pattern TABLE(
        [Id] [int] IDENTITY(1,1) NOT NULL PRIMARY KEY
        ,[Subject] [nvarchar](100) NULL
        ,[RecurrenceCount] [int] NULL
        ,[WeekDays] [varchar](max) NULL
        ,[StartDate] [datetime] NULL
        ,[EndDate] [datetime] NULL
        ,[Periodicity] [int] NULL
        ,[StartingWeek] [int] NULL
    );

    /*
        Populate with values based on Recurreance and Startdate. The startdate will give the start week, which make the start week obsolete.
    */
    DECLARE @PreferredDate TABLE(
        [Id] [int] IDENTITY(1,1) NOT NULL PRIMARY KEY
        ,[PreferredDate] [datetime] NULL
        ,[PreferredWeek] [int] NULL
        ,[PreferredYear] [int] NULL
    )

始终检索 datefirst 的当前设置非常重要。如果他们使用其他设置,您将破坏其他人的计算。出于显而易见的原因,我还添加了模式 ID。

    DECLARE @DateFirst int = @@dateFirst --DATEFIRST is a global setting

    DECLARE @mindate DATE       = (SELECT MIN(StartDate) FROM @Pattern WHERE id=@PreferredSubjectID)
    DECLARE @maxmindate DATE    = (SELECT MAX(StartDate) FROM @Pattern WHERE Id=@PreferredSubjectID)
    DECLARE @maxcount INT       = (SELECT MAX(RecurrenceCount) FROM @Pattern WHERE Id=@PreferredSubjectID) 
    DECLARE @maxdate DATE       = DATEADD(WK, @maxcount + 50, @maxmindate)

    SET DATEFIRST 1

    DECLARE @PreferredSubjectID int  = 1

@preferreddate table 使用以下内容填充:

    /*
        CTE to generate required preferred dates
    */
    ;With ctePreferredDate AS (
        Select  PreferredDate = @MinDate, PreferredWeek = DATEPART(WK, @MinDate), PreferredYear = DATEPART(YYYY, @MinDate)
        Union All
        SELECT  PreferredDate = DATEADD(WK,(SELECT Periodicity FROM @Pattern WHERE Id=@PreferredSubjectID), PreferredDate)
                ,PreferredWeek = DATEPART(WK,DATEADD(WK,(SELECT Periodicity FROM @Pattern WHERE Id=@PreferredSubjectID), PreferredDate))
                ,PreferredYear = DATEPART(yyyy,DATEADD(WK,(SELECT Periodicity FROM @Pattern WHERE Id=@PreferredSubjectID), PreferredDate))
        From ctePreferredDate pFD
        Where pFD.PreferredDate <= @MaxDate

    )
    INSERT INTO @PreferredDate (PreferredDate, PreferredWeek, PreferredYear)
    SELECT PreferredDate, PreferredWeek, PreferredYear
    FROM ctePreferredDate

最终的 CTE table 使用以下内容填充:

    /*
        CTE to generate required occurrences
    */
    ;With cteKeyDate As (
        Select  KeyStartDate = @MinDate 
                ,KeyDOW = DateName(WEEKDAY, @MinDate)
                ,KeyWeek = datepart(WK,@MinDate)
                ,id = @PreferredSubjectID
                ,KeyOccurrance = @maxcount
        Union All
        Select  KeyStartDate =  DateAdd(DD, 1, df.KeyStartDate)
                ,KeyDOW = DateName(WEEKDAY,DateAdd(DD, 1, df.KeyStartDate))
                ,KeyWeek= DatePart(WK,DateAdd(DD, 1, df.KeyStartDate))
                ,id=@PreferredSubjectID
                ,KeyOccurrance = @maxcount
        From cteKeyDate DF
        Where DF.KeyStartDate <= @MaxDate
    ) 
    SELECT  StartDate
            ,[DayOfWeek]
            ,[Week]
            ,OccNr = ROW_NUMBER() OVER         (PARTITION BY Id ORDER BY StartDate) 
    FROM
        (
        SELECT  cte.KeyStartDate AS StartDate
                ,cte.KeyDOW AS [DayOfWeek]
                ,cte.KeyWeek AS [Week]
                ,cte.id
                ,cte.KeyOccurrance AS Occurrance
                ,RowNr = ROW_NUMBER() OVER         (PARTITION BY KeyOccurrance ORDER BY KeyStartDate)
        FROM    cteKeyDate cte
                INNER JOIN
                @PreferredDate pfd
                    ON  cte.KeyWeek = pfd.PreferredWeek
                        AND YEAR(cte.KeyStartDate) = pfd.PreferredYear
        WHERE cte.KeyDOW IN (SELECT LTRIM(RTRIM(Item)) FROM fn_SplitString((SELECT weekdays from @Pattern WHERE Id=1),','))
        )cte
    WHERE   cte.RowNr <= cte.Occurrance
    ORDER BY cte.StartDate
    Option (maxrecursion 32767)

    SET DATEFIRST @DateFirst --Quite important

结果

2016/12/02  Friday  49  1
2016/12/03  Saturday    49  2
2016/12/04  Sunday  49  3
2017/01/06  Friday  2   4
2017/01/07  Saturday    2   5
2017/01/08  Sunday  2   6
2017/02/10  Friday  7   7
2017/02/11  Saturday    7   8
2017/02/12  Sunday  7   9
2017/03/17  Friday  12  10
2017/03/18  Saturday    12  11
2017/03/19  Sunday  12  12
2017/04/21  Friday  17  13
2017/04/22  Saturday    17  14
2017/04/23  Sunday  17  15
2017/05/26  Friday  22  16
2017/05/27  Saturday    22  17
2017/05/28  Sunday  22  18
2017/06/30  Friday  27  19
2017/07/01  Saturday    27  20
2017/07/02  Sunday  27  21
2017/08/04  Friday  32  22
2017/08/05  Saturday    32  23
2017/08/06  Sunday  32  24
2017/09/08  Friday  37  25
2017/09/09  Saturday    37  26
2017/09/10  Sunday  37  27
2017/10/13  Friday  42  28
2017/10/14  Saturday    42  29
2017/10/15  Sunday  42  30
2017/11/17  Friday  47  31
2017/11/18  Saturday    47  32
2017/11/19  Sunday  47  33
2017/12/22  Friday  52  34
2017/12/23  Saturday    52  35
2017/12/24  Sunday  52  36
2018/01/26  Friday  4   37
2018/01/27  Saturday    4   38
2018/01/28  Sunday  4   39
2018/03/02  Friday  9   40
2018/03/03  Saturday    9   41
2018/03/04  Sunday  9   42
2018/04/06  Friday  14  43
2018/04/07  Saturday    14  44
2018/04/08  Sunday  14  45
2018/05/11  Friday  19  46
2018/05/12  Saturday    19  47
2018/05/13  Sunday  19  48
2018/06/15  Friday  24  49
2018/06/16  Saturday    24  50
2018/06/17  Sunday  24  51
2018/07/20  Friday  29  52
2018/07/21  Saturday    29  53
2018/07/22  Sunday  29  54
2018/08/24  Friday  34  55
2018/08/25  Saturday    34  56
2018/08/26  Sunday  34  57
2018/09/28  Friday  39  58
2018/09/29  Saturday    39  59
2018/09/30  Sunday  39  60
2018/11/02  Friday  44  61
2018/11/03  Saturday    44  62
2018/11/04  Sunday  44  63
2018/12/07  Friday  49  64
2018/12/08  Saturday    49  65
2018/12/09  Sunday  49  66
2019/01/11  Friday  2   67
2019/01/12  Saturday    2   68
2019/01/13  Sunday  2   69
2019/02/15  Friday  7   70
2019/02/16  Saturday    7   71
2019/02/17  Sunday  7   72

拆分字符串函数:

ALTER FUNCTION [dbo].[fn_SplitString](
    @InputStr   varchar(Max),
    @Seperator  varchar(10))
RETURNS @OutStrings TABLE (ItemNo int identity(1,1), Item varchar(256))

AS
BEGIN

    DECLARE @Str varchar(2000),
            @Poz int, @cnt int

    --DECLARE @OutStrings TABLE (Item varchar(2000))

    SELECT @Poz = CHARINDEX (@Seperator, @InputStr), @cnt = 0
    WHILE @Poz > 0 AND @cnt <= 10000
    BEGIN
        SELECT @Str = SubString(@InputStr, 1, @Poz - 1)
        INSERT INTO @OutStrings(Item) VALUES(@Str)

        SELECT @InputStr = Right(@Inputstr, Len(@InputStr) - (len(@Str) + len(@Seperator)))
        SELECT @Poz = CHARINDEX (@Seperator, @InputStr), @cnt = @cnt + 1
    END
    IF @InputStr <> ''
    BEGIN
        INSERT INTO @OutStrings(Item) VALUES(@InputStr)
    END

    RETURN
END