【问题标题】:Add business days to date in SQL without loops在没有循环的 SQL 中添加迄今为止的工作日
【发布时间】:2011-03-29 11:07:04
【问题描述】:

我目前在我的 SQL 数据库中有一个函数,可以将一定数量的工作日添加到日期,例如如果您输入一个星期四的日期并添加两天,它将返回下一个星期一的日期。我不关心任何假期,只排除周末。

问题在于,目前这是使用 while 循环完成的,它似乎大大减慢了在生成表时使用它的存储过程。有谁知道是否有任何方法可以在没有 while 循环或游标的情况下执行此计算?

仅供参考,这是当前功能:

ALTER FUNCTION [dbo].[AddWorkDaysToDate]
(   
@fromDate       datetime,
@daysToAdd      int
)
RETURNS datetime
AS
BEGIN   
DECLARE @toDate datetime
DECLARE @daysAdded integer

-- add the days, ignoring weekends (i.e. add working days)
set @daysAdded = 1
set @toDate = @fromDate

while @daysAdded <= @daysToAdd
begin
    -- add a day to the to date
    set @toDate = DateAdd(day, 1, @toDate)
    -- only move on a day if we've hit a week day
    if (DatePart(dw, @toDate) != 1) and (DatePart(dw, @toDate) != 7)
    begin
        set @daysAdded = @daysAdded + 1
    end
end

RETURN @toDate

END

【问题讨论】:

  • 没有更短的解决方案。这段代码看起来不错,它应该不需要时间来执行。也许其他东西正在减慢...您可以通过删除此过程进行测试并查看性能是否有所提高。

标签: sql date


【解决方案1】:

如果有人正在寻找 TSQL 解决方案,这会更好。没有循环,没有表格,没有 case 语句并且适用于否定。谁能打败它?

CREATE FUNCTION[dbo].[AddBusinessDays](@Date date,@n INT)
RETURNS DATE AS 
BEGIN
DECLARE @d INT;SET @d=4-SIGN(@n)*(4-DATEPART(DW,@Date));
RETURN DATEADD(D,@n+((ABS(@n)+@d-2)/5)*2*SIGN(@n)-@d/7,@Date);
END

【讨论】:

  • 这个我用过,其他的好像没用过底片
  • 但是这个确实假设了一个特定的DATEFIRST 设置 (7),其他一些不需要。
  • 如果日期是周末则不起作用。 select dbo.AddBusinessDay('2015-02-01',-1) 应该返回 '2015-01-30' 但返回 '2015-01-28'
  • 如果传入的日期是星期日,则负值不起作用。
  • 嘿,我有 5 天的工作,但我想在星期五和星期六休息,那怎么样。你的代码没有给出正确的答案
【解决方案2】:

这个答案自被接受以来已经发生了重大变化,因为原来的答案是错误的。不过我对新查询更有信心,而且它不依赖于DATEFIRST


我认为这应该涵盖它:

declare @fromDate datetime
declare @daysToAdd int

select @fromDate = '20130123',@DaysToAdd = 4

declare @Saturday int
select @Saturday = DATEPART(weekday,'20130126')

;with Numbers as (
    select 0 as n union all select 1 union all select 2 union all select 3 union all select 4
), Split as (
    select @DaysToAdd%5 as PartialDays,@DaysToAdd/5 as WeeksToAdd
), WeekendCheck as (
    select WeeksToAdd,PartialDays,MAX(CASE WHEN DATEPART(weekday,DATEADD(day,n.n,@fromDate))=@Saturday THEN 1 ELSE 0 END) as HitWeekend
    from
    Split t
        left join
    Numbers n
        on
            t.PartialDays >= n.n
group by WeeksToAdd,PartialDays
)
select DATEADD(day,WeeksToAdd*7+PartialDays+CASE WHEN HitWeekend=1 THEN 2 ELSE 0 END,@fromDate)
from WeekendCheck

我们将要添加的时间分为数周和一周内的数天。然后,我们使用一个小数字表来计算增加这几天是否会导致我们遇到星期六。如果是这样,那么我们需要在总数上再增加 2 天。

【讨论】:

  • 非常好,效果很好。对于那些使用可能将 datefirst 设置为 7(即星期日)的英国 SQL 数据库的人,请记住将 WHEN 6 更改为 WHEN 7,并将 WHEN 7 更改为 WHEN 8。
  • @MattKing 我的 datefirst 也设置为 7,但您建议的更改不足以使其对我有用。请参阅我提出的解决方案的答案。
  • 此解决方案产生不正确的结果:即将 3 天添加到 1983 年 3 月 11 日星期五应该会生成 1983 年 3 月 16 日星期三。但是,此函数会生成 1983 年 3 月 14 日星期一。select @fromDate = '03-11-1983', @DaysToAdd = 3
  • @MartinDevillers 是对的。 2013 年 1 月 25 日星期五在今天的日期上添加一个工作日返回 1 月 28 日星期一。现在尝试添加 两个三个 个工作日。结果相同。
  • @Bernhof - 负数从来不是我正在研究的规范的一部分,尽管它可以扩展以涵盖它们
【解决方案3】:

此答案基于@ElmerMiller's answer

它修复了来自@FistOfFury 的周日评论的负值

如果传入的日期是星期日,则负值不起作用

还有来自@Damien_The_Unbeliever 的 DATEFIRST 设置评论

但是这个确实假设了一个特定的 DATEFIRST 设置 (7),其他一些不需要。

现在改正函数

CREATE FUNCTION[dbo].[AddBusinessDays](@Date DATE,@n INT)
RETURNS DATE AS 
BEGIN
DECLARE @d INT,@f INT,@DW INT;
SET @f=CAST(abs(1^SIGN(DATEPART(DW, @Date)-(7-@@DATEFIRST))) AS BIT)
SET @DW=DATEPART(DW,@Date)-(7-@@DATEFIRST)*(@f^1)+@@DATEFIRST*(@f&1)
SET @d=4-SIGN(@n)*(4-@DW);
RETURN DATEADD(D,@n+((ABS(@n)+(@d%(8+SIGN(@n)))-2)/5)*2*SIGN(@n)-@d/7,@Date);
END

【讨论】:

  • 它对我有用!但是,我不确定它是如何工作的。你能解释一下代码吗?
【解决方案4】:

根据该问题接受的答案,以下用户定义函数 (UDF) 应适用于所有情况 - 无论 @@DateFirst 的设置如何。

更新:正如下面的 cmets 所示,此函数设计为将 FromDate 设为工作日。当周末作为 FromDate 传入时,行为未定义。

ALTER FUNCTION [dbo].[BusinessDaysDateAdd] 
(
   @FromDate datetime,
   @DaysToAdd int
)
RETURNS datetime
AS
BEGIN
   DECLARE @Result datetime

   SET @Result = DATEADD(day, (@DaysToAdd % 5) + CASE ((@@DATEFIRST + DATEPART(weekday, @FromDate) + (@DaysToAdd % 5)) % 7)
                                                 WHEN 0 THEN 2
                                                 WHEN 1 THEN 1
                                                 ELSE 0 END, DATEADD(week, (@DaysToAdd / 5), @FromDate))

   RETURN @Result
END

【讨论】:

  • 只是一个注释,但我希望 BusinessDaysDateAdd('2013-09-01', 5) 返回 2013-09-06,但它返回 2013-09-09。
  • 我喜欢单线风格,但正如@EBarr 指出的那样,它不起作用。但是,将 CASE 更改为 CASE WHEN ((@@DATEFIRST + DATEPART(weekday, @FromDate)) % 7 + (@DaysToAdd % 5)) &gt; 6 THEN 2 ELSE 0 END 似乎可以解决问题。
  • 我对@Amine 之前的评论感到困惑。论坛将是 DATEADD(day, (@DaysToAdd % 5) + [在此处复制 Amine 的代码], DATEADD(week, (@DaysToAdd / 5), @FromDate))
  • 将 5 个工作日添加到星期日发生的日期,例如“2014-04-27”,应该返回下一个星期五,但此函数返回下一个星期一(添加 7 天而不是 5 )。
  • @ctorx 这与 EBarr 报告的行为相同。我认为可以肯定地说,这个函数是为 FromDate 设计的,是一个工作日。当周末作为 FromDate 传入时,行为未定义。
【解决方案5】:

您是否考虑过预先填充一个包含所有工作日的查找表(使用您的函数),例如 WorkingDays(int DaySequenceId, Date WorkingDate),然后您可以通过选择 DaySequenceId 来使用此表@fromDate 并添加 @daysToAdd 以获取新的工作日期。显然,此方法还具有管理 WorkingDays 表的额外开销,但您可以使用您期望的日期范围预先填充它。另一个缺点是可以计算的工作日期只会是包含在 WorkingDays 表中的那些。

【讨论】:

  • 一旦您开始处理工作日等,我同意通常最好添加某种日历表。
【解决方案6】:

*我知道这是一个旧线程,但不久前发现了一些非常有用的东西,对其进行了修改并得到了这个。

select ((DATEADD(d,DATEDIFF(d,0,(DATEADD (d,2,@fromDate))),@numbOfDays)))*

更新:我很抱歉匆忙找到一段代码(在单个语句中)并避免使用函数,我在这里发布了不正确的代码。

如果您添加的天数为 7 或更少,则可以使用上述位。

为了更好地理解,我已经用必要的参数更改了代码。

无论如何,我最终使用了上面提到的“Nate Cook”。并将其用作单行代码。 (因为我克制使用函数)

内特的代码

select(
DATEADD(day, (@days % 5) + 
CASE ((@@DATEFIRST + DATEPART(weekday, GETDATE()) + (@days % 5)) % 7)
WHEN 0 THEN 2
WHEN 1 THEN 1
ELSE 0 END, DATEADD(week, (@days / 5), GETDATE()))
)

【讨论】:

    【解决方案7】:

    为了扩展上面 Amine 的评论和 Nate Cook 的回答,对此的单线解决方案是:

    declare @DaysToAdd int , @FromDate datetime 
    set @DaysToAdd=-5      --5 days prior is 3/28/14
    set @FromDate='4/4/14'
    select 
        DATEADD(day, (@DaysToAdd % 5) 
        +   CASE 
            WHEN ((@@DATEFIRST + DATEPART(weekday, @FromDate)) % 7 + (@DaysToAdd % 5)) > 6 THEN 2 
            ELSE 0 
            END
        , DATEADD(week, (@DaysToAdd / 5), @FromDate))
    

    请注意,您可以在时间上分别增加或减少天数。

    【讨论】:

    • 到目前为止这是最准确的解决方案
    【解决方案8】:
    CREATE FUNCTION DateAddBusinessDays
    (
        @Days int,
        @Date datetime  
    )
    RETURNS datetime
    AS
    BEGIN
        DECLARE @DayOfWeek int;
    
        SET @DayOfWeek = CASE 
                            WHEN @Days < 0 THEN (@@DateFirst + DATEPART(weekday, @Date) - 20) % 7
                            ELSE (@@DateFirst + DATEPART(weekday, @Date) - 2) % 7
                         END;
    
        IF @DayOfWeek = 6 SET @Days = @Days - 1
        ELSE IF @DayOfWeek = -6 SET @Days = @Days + 1;
    
        RETURN @Date + @Days + (@Days + @DayOfWeek) / 5 * 2;
    END;
    

    无论@@DATEFIRST 的值如何,此函数都可以加减工作日。要减去工作日,请使用负数。

    【讨论】:

      【解决方案9】:

      我已经测试了这里提出的所有解决方案,但没有一个可行。 以下是一些测试场景,它们破坏了上述许多解决方案。 (假设周六和周日是您排除的日期):

      -将 0 天添加到星期六 - 预期结果 = 星期六

      -向星期日添加 0 天 - 预期结果 = 星期日

      -向星期五添加 1 天 - 预期结果 = 下一个星期一

      -星期六增加 1 天 - 预期结果 = 下星期一

      -周日加 1 天 - 预期结果 = 下周一

      -向星期五添加 3 天 - 预期结果 = 下一个星期三

      -星期六增加 5 天 - 预期结果 = 下一个星期五

      -将 5 天添加到星期五 - 预期结果 = 下一个星期五

      -从星期一减去 1 天 - 预期结果 = 上一个星期五

      -从星期日减去 1 天 - 预期结果 = 上一个星期五

      -从星期六减去 1 天 - 预期结果 = 上一个星期五

      -从星期一减去 3 天 - 预期结果 = 上一个星期三

      -从星期六减去 5 天 - 预期结果 = 前一个星期一

      -从星期一减去 5 天 - 预期结果 = 前一个星期一

      这是我在阅读了整个帖子并挑选了好的逻辑部分后写的:

      CREATE FUNCTION [dbo].[BusinessDateAdd]
      (
          @FromDate DATE
          ,@DaysToAdd INT
      )
      RETURNS DATE 
      AS 
      BEGIN
      
          --If there are no days to add or subtract, return the day that was passed in
          IF @DaysToAdd = 0 RETURN @FromDate
      
          DECLARE @Weeks INT
          DECLARE @DMod INT
          DECLARE @FromDateIndex INT
      
          --number of weeks
          SET @Weeks = @DaysToAdd/5
      
          --remainder of days
          SET @dmod = @DaysToAdd%5
      
          --Get the FromDate day of the week, this logic standardizes the @@DateFirst to Sunday = 1
          SET @FromDateIndex = (DATEPART(weekday, @FromDate) + @@DATEFIRST - 1) % 7 + 1
      
          /*Splitting the addition vs subtraction logic for readability*/
      
          --Adding business days
          IF @DaysToAdd > 0 
              BEGIN 
      
                  --If the FromDate is on a weekend, move it to the previous Friday
                  IF @FromDateIndex IN(1,7) 
                      BEGIN
                          SET @FromDate = DATEADD(dd,CASE @FromDateIndex WHEN 1 THEN -2 WHEN 7 THEN -1 END,@FromDate)
                          SET @FromDateIndex = 6
                      END
      
                  SET @FromDate = DATEADD(dd, 
                      CASE 
                          --If the mod goes through the weekend, add 2 days to account for it
                          WHEN 
                              ((@FromDateIndex = 3 --Tuesday
                              AND @dmod > 3) --Days until Friday
                              OR
                              (@FromDateIndex = 4  --Wednesday
                              AND @dmod > 2)--Days until Friday
                              OR 
                              (@FromDateIndex = 5 --Thursday
                              AND @dmod > 1)--Days until Friday
                              OR 
                              (@FromDateIndex = 6 --Friday
                              AND @dmod > 0))--Days until Friday
                              THEN 
                                  @DMod+2 
                          --Otherwise just add the mod
                          ELSE 
                              @DMod 
                      END, @FromDate)
      
              END
      
          --Subtracting business days
          IF @DaysToAdd < 0 
              BEGIN 
      
                  --If the FromDate is on a weekend, move it to the next Monday
                  IF @FromDateIndex IN(1,7) 
                      BEGIN
                          SET @FromDate = DATEADD(dd,CASE @FromDateIndex WHEN 1 THEN 1 WHEN 7 THEN 2 END,@FromDate)
                          SET @FromDateIndex = 2
                      END
      
                  SET @FromDate = DATEADD(dd, 
                      CASE 
                          --If the mod goes through the weekend, subtract 2 days to account for it
                          WHEN 
                              ((@FromDateIndex = 5 --Thursday
                              AND @dmod < -3) --Days until Monday
                              OR
                              (@FromDateIndex = 4  --Wednesday
                              AND @dmod < -2)--Days until Monday
                              OR 
                              (@FromDateIndex = 3 --Tuesday
                              AND @dmod < -1)--Days until Monday
                              OR 
                              (@FromDateIndex = 2 --Monday
                              AND @dmod < 0))--Days until Monday
                              THEN 
                                  @DMod-2 
                          --Otherwise just subtract the mod
                          ELSE 
                              @DMod 
                      END, @FromDate)
      
              END
      
          --Shift the date by the number of weeks
          SET @FromDate = DATEADD(ww,@Weeks,@FromDate)
      
          RETURN @FromDate
      
      END
      

      【讨论】:

        【解决方案10】:

        我目前没有要测试的 Sql Server,但这是我的想法:

        ALTER FUNCTION [dbo].[AddWorkDaysToDate]
        (   
        @fromDate       datetime,
        @daysToAdd      int
        )
        RETURNS datetime
        AS
        BEGIN   
        DECLARE @dw integer
        DECLARE @toDate datetime
        
        set datefirst 1
        set @toDate = dateadd(day, @daysToAdd, @fromDate)
        set @dw = datepart(dw, @toDate)
        
        if @dw > 5 set @toDate = dateadd(day, 8 - @dw, @toDate)
        
        RETURN @toDate
        
        END
        

        【讨论】:

          【解决方案11】:

          感谢 Damien 提供代码。计算中有一个小错误,因为它只为星期日增加了 1 天,并且当工作日数超过周末(但未在周末着陆)时,额外的 2 天没有考虑在内。这是 Damiens 代码的修改版本,它使用默认的 datefirst 在 7。希望这会有所帮助。

          CREATE FUNCTION [dbo].[fn_AddBusinessDays]  
          (  
              @StartDate datetime,  
              @BusinessDays int  
          ) 
          RETURNS datetime  
          AS  
          BEGIN 
          DECLARE @EndDate datetime
          
          SET @EndDate = DATEADD(day, @BusinessDays%5 + 
                     CASE         
                  WHEN DATEPART(weekday,@StartDate) +  @BusinessDays%5 > 6 THEN 2                  
                  ELSE 0 
                     END,     
             DATEADD(week,@BusinessDays/5,@StartDate))    
          
             RETURN @EndDate
          END  
          GO
          

          【讨论】:

          • 我遇到了和@Evergreen435 一样的问题。我发布了适用于 DateFirst 的所有设置的答案。
          【解决方案12】:

          问题的已接受答案会产生不正确的结果。例如。 select @fromDate = '03-11-1983', @DaysToAdd = 3 产生 03-14-198303-16-1983expected

          我发布了一个有效的解决方案here,但为了完整起见,我也会在此处添加它。如果您对这两种方法的详细信息感兴趣,请访问我的原始答案。如果没有,只需将其复制/粘贴到您的 SQL 项目中并使用 UTL_DateAddWorkingDays

          请注意,我的解决方案仅在 DATEFIRST 设置为默认值 7 时才有效。

          Test Script used to test various methods

          CREATE FUNCTION [dbo].[UTL_DateAddWorkingDays]
          (   
              @date datetime,
              @days int
          )
          RETURNS TABLE AS RETURN 
          (
              SELECT 
                  CASE 
                      WHEN @days = 0 THEN @date
                      WHEN DATEPART(dw, @date) = 1 THEN (SELECT Date FROM [dbo].[UTL_DateAddWorkingDays_Inner](DATEADD(d, 1, @date), @days - 1))
                      WHEN DATEPART(dw, @date) = 7 THEN (SELECT Date FROM [dbo].[UTL_DateAddWorkingDays_Inner](DATEADD(d, 2, @date), @days - 1))
                      ELSE (SELECT Date FROM [dbo].[UTL_DateAddWorkingDays_Inner](@date, @days))
                  END AS Date
          )
          
          CREATE FUNCTION [dbo].[UTL_DateAddWorkingDays_Inner]
          (   
              @date datetime,
              @days int
          )
          RETURNS TABLE AS RETURN 
          (
              SELECT 
                  DATEADD(d
                  , (@days / 5) * 7 
                    + (@days % 5) 
                    + (CASE WHEN ((@days%5) + DATEPART(dw, @date)) IN (1,7,8,9,10) THEN 2 ELSE 0 END)
                  , @date) AS Date
          )
          

          【讨论】:

            【解决方案13】:

            这是我用的:

            SET DATEFIRST 1;
            
            SELECT DATEADD(dw, (**NumberToAdd**/5)*7+(**NumberToAdd** % 5) + 
            (CASE WHEN DATEPART(dw,**YourDate**) + (**NumberToAdd** % 5) > 5 
            THEN 2 ELSE 0 END), **YourDate**) AS IncrementedDate
            FROM YourTable t
            

            “SET DATEFIRST 1;”部分是必要的,将星期一设置为一周的第一天。

            【讨论】:

              【解决方案14】:
              WITH get_dates
              AS
              (
                  SELECT getdate() AS date, 0 as DayNo 
                  UNION ALL
                  SELECT date + 1 AS date, case when DATEPART(DW, date + 1) IN (1,7) then DayNo else DayNo + 1 end
                  FROM get_dates
                  WHERE DayNo < 4
              )
              SELECT max(date) FROM get_dates
              OPTION (MAXRECURSION 0) 
              

              【讨论】:

              • 将 getdate() 更改为开始日期,将 4 更改为没有天数
              • 欢迎来到 SO。将 cmets 添加到您的代码中以解释其工作原理确实有助于新用户。
              【解决方案15】:

              这是一个旧线程,但我刚刚创建了一个包含所有日期的表格,然后这样做了:

              SELECT Count(*) 
              FROM Date_Table
              WHERE [day] BETWEEN @StartDate and @EndDate
                  AND DATENAME(weekday, [day]) NOT IN ('Sunday', 'Saturday')
              

              【讨论】:

                【解决方案16】:

                我从 Microsoft Docs 中发现了一种更优雅的方法。它考虑了跳过多个周末。超级干净。

                CREATE FUNCTION DAYSADDNOWK(@addDate AS DATE, @numDays AS INT)
                RETURNS DATETIME
                AS
                BEGIN
                    WHILE @numDays>0
                    BEGIN
                       SET @addDate=DATEADD(d,1,@addDate)
                       IF DATENAME(DW,@addDate)='saturday' SET @addDate=DATEADD(d,1,@addDate)
                       IF DATENAME(DW,@addDate)='sunday' SET @addDate=DATEADD(d,1,@addDate)
                
                       SET @numDays=@numDays-1
                    END
                
                    RETURN CAST(@addDate AS DATETIME)
                END
                GO
                

                运行测试

                SELECT dbo.DAYSADDNOWK(GETDATE(), 15)
                

                【讨论】:

                  【解决方案17】:

                  我知道这有点晚了,也许其他人偶然发现了这个问题。 上面的方法我试过了,但是大部分都不能计算假期。

                  这就是我的尝试

                  CREATE function [dbo].[DateAddWorkDay]
                  (@days int,@FromDate Date)
                  returns Date
                  as
                  begin
                  declare @result date
                  set @result = (
                  select b
                  from
                  (
                      SELECT
                      b,
                         (DATEDIFF(dd, a, b))
                        -(DATEDIFF(wk, a, b) * 2)
                        -(CASE WHEN DATENAME(dw, a) = 'Sunday' THEN 1 ELSE 0 END)
                        -(CASE WHEN DATENAME(dw, b) = 'Saturday' THEN 1 ELSE 0 END)
                        -COUNT(o.Holiday_Date) 
                        as workday
                      from
                      (
                      select 
                      @FromDate as a,
                      dateadd(DAY,num +@days,@FromDate) as b
                      from (select row_number() over (order by (select NULL)) as num
                            from Information_Schema.columns
                           ) t
                      where num <= 100 
                      ) dt
                      left join Holiday o on o.Holiday_Date between a and b and DATENAME(dw, o.Holiday_Date) not in('Saturday','Sunday') 
                      where DATENAME(dw, b) not in('Saturday','Sunday')
                            and b not in (select Holiday_Date from OP_Holiday where Holiday_Date between a and b) 
                  
                      group by a,b
                  ) du
                  where workday =@days 
                  
                  
                  )
                  return @result 
                  end
                  

                  Holiday 是一个以 holiday_date 作为假期参考的表格

                  希望这可以帮助一些人。

                  【讨论】:

                    【解决方案18】:

                    此 SQL 函数的工作方式类似于 Excel WORKDAY 函数。 希望对你有帮助。

                    CREATE FUNCTION [dbo].[BusDaysDateAdd] 
                    (
                       @FromDate date,
                       @DaysToAdd int
                    )
                    RETURNS date
                    AS
                    BEGIN
                       DECLARE @Result date
                       DECLARE @TempDate date
                       DECLARE @Remainder int
                       DECLARE @datePartValue int
                    
                       SET @TempDate = (DATEADD(week, (@DaysToAdd / 5), @FromDate))
                       SET @Remainder = (@DaysToAdd % 5)
                       SET @datePartValue = DATEPART(weekday, @TempDate)
                       SET @Result = DATEADD(day,@Remainder + CASE WHEN @Remainder > 0 AND @datePartValue = 7 THEN 1
                                                                    WHEN @Remainder >= 1 AND @datePartValue = 6 THEN 2
                                                                    WHEN @Remainder >= 2 AND @datePartValue = 5 THEN 2
                                                                    WHEN @Remainder >= 3 AND @datePartValue = 4 THEN 2
                                                                    WHEN @Remainder >= 4 AND @datePartValue = 3 THEN 2
                                                                    WHEN @Remainder >= 5 AND @datePartValue = 2 THEN 2
                                                                    ELSE 0 END, @TempDate)
                       RETURN @Result
                    END
                    GO
                    

                    Reference

                    【讨论】:

                      【解决方案19】:

                      我参加这个聚会有点晚了,但由于其他解决方案的缺点,我最终编写了自己的版本。具体来说,这个版本解决了倒数问题,并从周末开始。

                      如果您将零工作日添加到周末日期,可能会出现模棱两可的情况。我将日期保持不变,但如果您总是想强制返回工作日,则可以省略此检查。

                      CREATE FUNCTION [dbo].[fn_AddBusinessDays]
                      (
                          @date datetime,
                          @businessDays int
                      )
                      RETURNS datetime
                      AS
                      BEGIN
                          --adjust for weeks first
                          declare @weeksToAdd int = @businessDays / 7
                          declare @daysToAdd int = @businessDays % 7
                      
                          --if subtracting days, subtract a week then offset
                          if @businessDays < 0 begin
                              set @daysToAdd = @businessDays + 5
                              set @weeksToAdd = @weeksToAdd - 1
                          end
                      
                          --saturday becomes zero using the modulo operator
                          declare @originalDayOfWeek int = datepart(dw, @date) % 7
                          declare @newDayOfWeek int = datepart(dw, dateadd(d, @daysToAdd, @date)) % 7
                      
                          --special case for when beginning date is weekend
                          --adding zero on a weekend keeps the same date. you can remove the <> 0 check if you want Sunday + 0 => Monday
                          declare @dateOffset int = case
                              when @businessDays <> 0 and @originalDayOfWeek = 0 then 2
                              when @businessDays <> 0 and @originalDayOfWeek = 1 then 1
                              when @businessDays <> 0 and @newDayOfWeek < @originalDayOfWeek then 2
                              else 0
                          end
                      
                          -- Return the result of the function
                          return dateadd(d, @daysToAdd + @dateOffset, dateadd(ww, @weeksToAdd, @date))
                      
                      END
                      

                      【讨论】:

                        【解决方案20】:

                        我最近解决了这个问题,通过创建一个 INT 值 @DaysToAdd 将两个工作日添加到当前日期 - 在 2008 年 / 2012 年测试并运行良好。

                        DECLARE @DaysToAdd INT
                        
                        SELECT @DaysToAdd = CASE  
                           WHEN DATEPART(WEEKDAY,GETDATE()) =  1 THEN 3 -- Sunday -> Wednesday
                           WHEN DATEPART(WEEKDAY,GETDATE()) =  5 THEN 4 -- Thursday -> Monday
                           WHEN DATEPART(WEEKDAY,GETDATE()) =  6 THEN 4 -- Friday -> Tuesday
                           WHEN DATEPART(WEEKDAY,GETDATE()) =  7 THEN 4 -- Saturday -> Wednesday
                           ELSE 2 END
                        
                        SELECT DATEADD(DAY, @DaysToAdd, GETDATE()) AS TwoWorkingDaysTime
                        

                        【讨论】:

                        • 该问题询问有关将任意天数添加到开始日期的问题。您的解决方案专为增加 2 天而量身定制。
                        【解决方案21】:

                        我刚刚测试了接受的答案,发现周日是开始日时它不起作用。

                        您需要在Select @Saturday 行项下添加以下内容:

                        SELECT @fromDate = CASE WHEN DATEPART(weekday,@fromDate) = 1 THEN DATEADD(day,1,@fromDate) ELSE @fromDate END
                        

                        【讨论】:

                          【解决方案22】:

                          叹息。我不敢相信这么多年过去了仍然没有:a) Microsoft SQL Server 中的标准“DateAddWorkDays”(即使 Microsoft 在 Excel 中永远有一个 WorkDay 函数)和 b) 明确的解决方案在这里或其他任何我能找到的可以处理人们提出的所有问题的地方。

                          这是我开发的一个解决方案,它解决了以下问题,我在这里和其他地方找到的所有上述答案似乎都有一个或多个。这处理:

                          1. 助记标识符名称。
                          2. 注释解释不清楚的代码。
                          3. 不检查需要增加的每个工作日(即 远低于 O(n) 复杂度)。
                          4. 负工作日增量。
                          5. 允许传入非上午 12 点的时间部分(这样您就不必先删除它)。
                          6. 在结果中保留传入的时间部分(如果有)(以防您需要提前/前 x 个工作日的确切时间)。
                          7. 非英语语言的周末名称。
                          8. @@DateFirst 值不是默认值(7 aka U.S.)。
                          9. 指定非周末非工作日的自定义列表。
                          10. 如果传入日期的时间不是上午 12 点,则允许非周末非工作日列表工作。
                          11. 如果 # 个工作日增量为 0,则返回开始日期时间,即使开始日期时间在非工作日也是如此。
                          12. 在开始分别增加/减少工作日之前先移动到下一个/上一个工作日。注意:这与 Excel 的 WorkDay 函数不同,但我相信这更有用和更直观。前任。如果您在周末收到询价/订单,并且您的 SLA(即响应时间、交付日期)为 1 个工作日,则您不必在 1 个完整工作日过去之前响应/交付(无论如何许多相邻的非工作日之前)。
                          13. 在添加任何非工作工作日后可能已经跨越的任何额外周末和/或非工作工作日跳过,当仅添加 # 个工作日时,添加初始周末可能跨越 - 并重复直到没有需要更长的时间。

                          建议:当然,与任何递归算法一样,这个算法可以转换为迭代算法(通过实现您自己的堆栈,即使用临时表),但我认为 32 个嵌套级别对于绝大多数现实世界的用例。此外,当然,您可以通过将非工作工作日日期作为表值参数而不是硬编码的表参考传递来使其更通用/可移植。

                          SET ANSI_NULLS ON
                          GO
                          
                          SET QUOTED_IDENTIFIER ON
                          GO
                          
                          -- ===================================================================================================================================
                          -- Author:      Tom
                          -- Create date: 03/13/2017
                          -- Description: Add specified # of working days (+/-) to a specified date-time assuming existence of a list of non-work weekday 
                          --  dates (incl. holidays, weather days, utility outage days, fire days, etc.) in the 'NonWorkDayDate' Column of a 'NonWorkWeekday' 
                          --  Table. If specified # working days is 0, the specified date-time is returned.  Working days are not added until the specified 
                          --  date-time has first been incremented (+/-) to the next working day in the direction of the working days increment.
                          --  NOTE: Uses a forumla (vs. O(n) loop) that uses recusion whenever days incremented (incl. weekends) spans non-work weekdays.
                          --  !!!WARNING!!!: Will exceed SQL Server nesting level (32) if abs (# of working days) < ~1 / 32 adjacent non-working days.
                          -- Parameters:
                          --  @RefDateTime    DateTime:   Reference date-time to which to add '@WorkDaysIncrement'.
                          --  @WorkDaysIncrement  Int:    # of working days (+/-) to add # to the '@RefDateTime'.
                          -- Returns:
                          --  1. Result of @RefDateTime + @WorkDaysIncrement (skipping weekend and holiday dates and retaining the @RefDateTime's time).
                          -- ===================================================================================================================================
                          CREATE FUNCTION [dbo].[AddWorkDays_Recursive] 
                          (
                              -- Add the parameters for the function here
                              @RefDateTime datetime,
                              @WorkDaysIncrement int
                          )
                          RETURNS DateTime
                          AS
                          BEGIN
                          
                          -- If no days to increment, return passed in date-time (even if weekend day).
                              if (@WorkDaysIncrement = 0) return @RefDateTime
                          
                          -- Set the one-day increment used to add or subtract one calendar/work day.
                              declare @OneDayIncrement int = sign(@WorkDaysIncrement)
                          
                          -- Initialize # of calendar days added to 0.
                              declare @DaysAdded int = 0
                          
                          -- Set reference date to date (i.e. excl. time) of reference date-time.
                              declare @RefDate datetime = convert
                                  (
                                      date,
                                      convert
                                      (
                                          varchar(10),
                                          @RefDateTime,
                                          101
                                      )
                                  ) 
                              --end declare @RefDate 
                          
                          -- Initialize result date to reference date
                              declare @ResultDate datetime = @RefDate
                          
                          -- Set U.S. Weekday # to the 1-based U.S. weekday # result date.
                              declare @USWeekdayNumber tinyint = ((datepart(weekday, @ResultDate) + @@datefirst - 1) % 7) + 1 -- Sun to Sat = 1 to 7
                          
                          -- If result date is now on a weekend day, set #  of weekend days increment so that we can move it +/- 1 to 2 days to next weekday.
                              declare @WeekendDaysInc smallint = 
                              (
                                  case (@USWeekdayNumber)
                          
                                      when 1 then --Sunday 
                                          case
                                              when (@OneDayIncrement > 0) then 1
                                              else -2 
                                          end 
                                      --end when 1 --Sunday
                          
                                      when 7 then --Saturday 
                                          case
                                              when (@OneDayIncrement > 0) then 2
                                              else -1
                                          end 
                                      --end when 7 then --Saturday 
                          
                                      else 0 -- Not Weekend Day #
                          
                                  end -- case (@USWeekdayNumber)
                              ) -- end declare @WeekendDaysInc smallint = 
                          
                          -- Increment # of calendar days added by #  of weekend days increment
                              set @DaysAdded += @WeekendDaysInc
                          
                          -- Increment result date by #  of weekend days increment
                              set @ResultDate += @WeekendDaysInc 
                          
                          -- Set # of work weeks increment to # of full 5-day increments in the # (+/-) of work days to increment.
                              declare @WorkWeeksIncrement int = @WorkDaysIncrement / 5
                          
                          -- Increment # of calendar days added by 7 times # of work weeks increment, i.e. to add weekday + weekend days for full weeks.
                              set @DaysAdded += @WorkWeeksIncrement * 7
                          
                          -- Set result date after full weeks added to reference date + # of calendar days 
                              declare @AfterFullWeeksResultDate datetime = @ResultDate + @DaysAdded
                          
                          -- Set # partial-work week days to # (+/-) of work days to increment left after adding full weeks.
                              declare @PartialWorkWeekDays int = @WorkDaysIncrement % 5
                          
                          -- Increment # of calendar days added by # partial-work week days
                              set @DaysAdded += @PartialWorkWeekDays
                          
                          -- Set result date after partial week added to result date after full weeks added + # partial work week days
                              declare @AfterPartialWeekResultDate datetime = @AfterFullWeeksResultDate + @PartialWorkWeekDays
                          
                          --Set result date to result date after partial week.
                              set  @ResultDate = @AfterPartialWeekResultDate
                          
                          -- Set After Full Weeks U.S. Weekday # to the 1-based U.S. weekday # result date.
                              declare @AfterFullWeeksUSWeekdayNumber tinyint = 
                                  (
                                      ((datepart(weekday, @AfterFullWeeksResultDate) + @@datefirst - 1) % 7) + 1 -- Sun to Sat = 1 to 7
                                  )
                          
                          -- Set After Partial Week U.S. Weekday # to the 1-based U.S. weekday # result date.
                              declare @AfterPartialWeekUSWeekdayNumber tinyint = 
                                  (
                                      ((datepart(weekday, @AfterPartialWeekResultDate) + @@datefirst - 1) % 7) + 1 -- Sun to Sat = 1 to 7
                                  )
                          
                          --If (incrementing and After Full Weeks U.S. Weekday # > @AfterPartialWeekUSWeekdayNumber) 
                          --  or (decrementing and After Full Weeks U.S. Weekday # < @AfterPartialWeekUSWeekdayNumber), increment by (+/-) 2 to account for 
                          --  the weekend that was spanned when partial-work week days were added.
                              if 
                              (
                                  (
                                      (@OneDayIncrement > 0)
                                      and (@AfterFullWeeksUSWeekdayNumber > @AfterPartialWeekUSWeekdayNumber)
                                  )
                                  or (
                                      (@OneDayIncrement < 0)
                                      and (@AfterFullWeeksUSWeekdayNumber < @AfterPartialWeekUSWeekdayNumber)
                                  )
                          
                              )
                              begin
                                  set @WeekendDaysInc = 2 * @OneDayIncrement
                                  set @DaysAdded += @WeekendDaysInc
                                  set @ResultDate += @WeekendDaysInc
                              end -- if need to increment to account for weekend spanned by partial-work week days,
                          
                          -- Set U.S. Weekday # to the 1-based U.S. weekday # result date.
                              set @USWeekdayNumber = ((datepart(weekday, @ResultDate) + @@datefirst - 1) % 7) + 1 -- Sun to Sat = 1 to 7
                          
                          -- If result date is now on a weekend day, set #  of weekend days increment so that we can move it +/- 1 to 2 days to next weekday.
                              set @WeekendDaysInc = 
                              (
                                  case (@USWeekdayNumber)
                          
                                      when 1 then --Sunday 
                                          case
                                              when (@OneDayIncrement > 0) then 1
                                              else -2 
                                          end 
                                      --end when 1 --Sunday
                          
                                      when 7 then --Saturday 
                                          case
                                              when (@OneDayIncrement > 0) then 2
                                              else -1
                                          end 
                                      --end when 7 then --Saturday 
                          
                                      else 0 -- Not Weekend Day #
                          
                                  end -- case (@USWeekdayNumber)
                              ) -- end declare @WeekendDaysInc smallint = 
                          
                          -- Increment # of calendar days added by #  of weekend days increment
                              set @DaysAdded += @WeekendDaysInc
                          
                          -- Increment result date by #  of weekend days increment
                              set @ResultDate += @WeekendDaysInc 
                          
                          -- Set non-work weedays count to # Rows where NonWorkDayDate between RefDate and ResultDate (if # of work days to increment > 0), else between 
                          --  ResultDate and RefDate.
                              declare @NonWorkWeekdaysCount int =
                              (
                                  select count(nw.NonWorkDayDate) 
                                      from NonWorkWeekday as nw
                                      where 
                                          (
                                              (@OneDayIncrement > 0)
                                              and (nw.NonWorkDayDate between @RefDate and @ResultDate)
                                          )
                                          or (
                                              (@OneDayIncrement < 0)
                                              and (nw.NonWorkDayDate between @ResultDate and @RefDate)
                                          )
                                  --end select count(nw.NonWorkDayDate) from Holidate as nw 
                              ) -- end declare @HolidaysSpanned int =
                          
                          -- Set result date-time to reference date-time + # of calendar days added
                              declare @ResultDateTime datetime = @RefDateTime + @DaysAdded 
                          
                          -- Set result date-time equal to result of adding (# of holidays x one-day increment).
                              set @ResultDateTime = dbo.AddWorkDays_Recursive
                                  (
                                      @ResultDateTime, -- @RefDateTime
                                      @NonWorkWeekdaysCount * @OneDayIncrement -- @WorkDaysIncrement
                                  )
                              --end set @ResultDateTime = 
                          
                              -- Return the result of the function
                              RETURN @ResultDateTime
                          
                          END
                          
                          GO
                          

                          【讨论】:

                          • 糟糕!从与周末和 1 个以上非工作工作日的组合相邻的一天开始不工作! Prolly 还需要跳过非工作日(除了周末),通过调用我用 WorkDaysIncrement = +/-1 和 NonWorkDayAsDay0 = 0 编写的“AddWorkDays_Incremental”函数开始,从这个 WorkDaysIncrement 中减去 +/-1 天,改变计数的非工作日是开始日期的排他性(与包含性),并使该开始日期为 RefDate 跳过最初的周末/非工作日与仅 RefDate。可能还会避免嵌套限制。
                          【解决方案23】:

                          对于德国,所有答案都行不通。

                          我测试和工作的唯一功能是从旧的 Excel 表格 here 的翻译:

                          Set @EndDate=Dateadd(DAY,@DaysToAdd,@FromDate) +
                          Cast(((
                                   CASE WHEN 5 <= DATEPART(weekday, @FromDate)%7 
                                  THEN 5
                                   ELSE 
                                   DATEPART(weekday, @FromDate)%7
                                   END)
                                -1 + @DaysToAdd )/5 
                           as int) 
                          * 2 - 
                             (Case when DAtepart(weekday, @FromDate)=6 then 1 else 0 end) 
                          

                          【讨论】:

                            【解决方案24】:

                            --重构我的原始答案...如果开始日期恰好是周末,我添加了定义计算起点的选项:从那个周末开始或转移到最近的工作日,具体取决于三角洲的方向。

                            DECLARE
                                @input DATE = '2019-06-15', -- if null, then returns null
                                @delta INT = 1, -- can be positive or negative; null => zero
                                @startFromWeekend BIT = 1 -- null => zero
                            
                            -- input is null, delta is zero/null
                            IF @input IS NULL OR ISNULL(@delta, 0) = 0
                                SELECT @input
                            
                            -- input is not null and has delta
                            ELSE
                            BEGIN
                                DECLARE
                                    @input_dw INT = (DATEPART(DW, @input) + @@DATEFIRST - 1) % 7, -- input day of week
                                    @weeks    INT = @delta / 5, -- adjust by weeks
                                    @days     INT = @delta % 5  -- adjust by days
                            
                                -- if input is a weekend day, offset it for proper calculation
                                -- !!important!!: depends on *your* definition of the starting date to perform calculation from
                                DECLARE @offset INT =
                                    -- start calc from weekend day that is nearest to a weekday depending on delta direction
                                    -- pos delta: effectively Sunday of the weekend   (actual: prev Friday)
                                    -- neg delta: effectively Saturday of the weekend (actual: next Monday)
                                    CASE WHEN ISNULL(@startFromWeekend, 0) = 1
                                    THEN CASE WHEN @delta > 0
                                        THEN CASE @input_dw
                                            WHEN 0 THEN -2
                                            WHEN 6 THEN -1
                                            END
                                        ELSE CASE @input_dw
                                            WHEN 0 THEN  1
                                            WHEN 6 THEN  2
                                            END
                                        END
                                    -- start calc from nearest weekday depending on delta direction
                                    -- pos delta: next Monday from the weekend
                                    -- neg delta: prev Friday from the weekend
                                    ELSE CASE WHEN @delta > 0
                                        THEN CASE @input_dw
                                            WHEN 0 THEN  1
                                            WHEN 6 THEN  2
                                            END
                                        ELSE CASE @input_dw
                                            WHEN 0 THEN -2
                                            WHEN 6 THEN -1
                                            END
                                        END
                                    END
                            
                                -- calculate: add weeks, add days, add initial correction offset
                                DECLARE @output DATE = DATEADD(DAY, @days + ISNULL(@offset, 0), DATEADD(WEEK, @weeks, @input))
                            
                                -- finally, if output is weekend, add final correction offset depending on delta direction
                                SELECT
                                    CASE WHEN (DATEPART(DW, @output) + @@DATEFIRST - 1) % 7 IN (0,6)
                                    THEN CASE 
                                        WHEN @delta > 0 THEN DATEADD(DAY,  2, @output)
                                        WHEN @delta < 0 THEN DATEADD(DAY, -2, @output)
                                        END
                                    ELSE @output
                                    END
                            END
                            

                            【讨论】:

                              【解决方案25】:

                              我找不到一个我能理解的令人满意的解决方案,所以我最终主要是自己写一个。这开始在结构上类似于Damien_The_Unbeliever's answer,但由于我无法使其工作而产生了很大的分歧。

                              我的要求

                              • 我正在使用 Redshift,所以必须在那里工作。
                              • 负的天数输入必须有效(例如,添加 -1 或 -12 个工作日)。
                              • 当输入日期为工作日时(例如周一 + 2 → 周三;周二 - 4 → 上周三),输出必须正确。
                              • 必须记录解决方案。

                              锦上添花

                              • 周末输出正常。 (我选择将周末滚动到下一个星期一,例如,星期日 + 1 == 星期一 + 1 == 星期二。)

                              解决方案

                              注意:我的公司将Periscope Data 用于 BI,它具有类似 C 宏的语法糖来定义它称为 Snippets 的内联文本替换(请参阅 docs)。不过,应该很容易翻译成纯 SQL——如果你已经完成了翻译,请随时建议对我的答案进行编辑。

                              片段:add_business_days(date,num_days)

                              (dateadd(
                                day
                                , (
                                  7 * (([num_days]) / 5)                                    -- add whole weeks
                                  + (([num_days]) % 5)                                      -- add remaining days after taking out whole weeks
                                  + case when (                                             -- if (
                                      extract(dow from [roll_forward_to_weekday("[date]")]) --   day of week of "rolled forward" date (i.e. weekends → Monday)
                                       + (([num_days]) % 5)                                 --   + remaining days after taking out whole weeks
                                      not between 1 and 5                                   --   is NOT a weekday of the same week
                                    )                                                       -- )
                                      then sign([num_days])::int * 2                        -- then increase magnitude of num_days by 2 to jump over the weekend
                                      else 0
                                    end
                                )                                                           -- start from the "rolled forward" date because adding business days to ..
                                , [roll_forward_to_weekday("[date]")]                       -- Saturday or Sunday is equivalent to adding them to the following Monday.
                                                                                            -- (note: due to ^, add_business_days(Saturday or Sunday,0) == Monday)
                              ))
                              

                              片段:roll_forward_to_weekday(date)

                              (dateadd(
                                day
                                , case extract(dayofweek from([date]))
                                    when 6 /* Saturday */ then 2
                                    when 0 /* Sunday   */ then 1
                                                          else 0
                                  end
                                , ([date])
                              ))
                              

                              【讨论】:

                                猜你喜欢
                                • 1970-01-01
                                • 1970-01-01
                                • 1970-01-01
                                • 2013-03-06
                                • 1970-01-01
                                • 1970-01-01
                                • 1970-01-01
                                • 2018-08-01
                                • 2012-07-08
                                相关资源
                                最近更新 更多