Add business days to date in SQL without loops

后端 未结 25 2755
无人共我
无人共我 2020-12-02 22:54

I currently have a function in my SQL database that adds a certain amount of business days to a date, e.g. if you enter a date that is a Thursday and add two days, it will r

相关标签:
25条回答
  • 2020-12-02 23:27

    This SQL function works similar to Excel WORKDAY function. Hope it can help you.

    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

    0 讨论(0)
  • 2020-12-02 23:27

    I'm a little late to this party but I wound up writing my own version of this, because of drawbacks in the other solutions. Specifically this version addresses counting backwards, and starting on weekends.

    There's an ambiguous situation that could arise, if you add zero business days to a weekend date. I've kept the date the same, but you can leave out this check if you always want to force a weekday to be returned.

    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
    
    0 讨论(0)
  • 2020-12-02 23:27

    I just tested the accepted answer and found that it does not work when Sunday is the start day.

    You need to add the following under the Select @Saturday line item:

    SELECT @fromDate = CASE WHEN DATEPART(weekday,@fromDate) = 1 THEN DATEADD(day,1,@fromDate) ELSE @fromDate END
    
    0 讨论(0)
  • 2020-12-02 23:27

    For Germany all of the answers don't work.

    The only function I tested and works is a translation from an old Excel form 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) 
    
    0 讨论(0)
  • 2020-12-02 23:31

    Sigh. I can't believe after all these decades there's still no : a) standard "DateAddWorkDays" in Microsoft SQL Server (even though Microsoft has had a WorkDay Function in Excel forever) and b) clear solution in here or anywhere else I can find that handles all issues people have raised.

    Here's a solution I developed that addresses the following issues that seemingly all the above answers here and elsewhere I've been able to find has one or more of. This handles:

    1. Mnemonic identifier names.
    2. Comments explaining code that's not clear.
    3. Not checking every single work day needing to be incremented (i.e. much less than O(n) complexity).
    4. Negative work day increments.
    5. Allowing non-12 am time portion to be passed in (so you won't have to strip it first).
    6. Retaining the passed-in time portion, if any, in the result (in case you need the exact time x-business days ahead/ago).
    7. Weekend day names in languages other than English.
    8. @@DateFirst values other than the default (7 aka U.S.).
    9. Specifying a custom list of non-weekend non-working days.
    10. Allowing list of non-weekend non-working days to work if passed-in date has a non-12 am time.
    11. Returning starting date-time if # work days increment is 0 even if starting date-time is on a non-working day.
    12. Moving to the next / previous working day first before starting to increment / decrement working days, respectively. NOTE: This differs from Excel's WorkDay Function, but I believe this is more useful and intuitive. Ex. If you get an inquiry / order on a weekend day, and you have an SLA (i.e. response time, delivery date) of 1 business day, you shouldn't have to respond / deliver until 1 full working day has passed (regardless of how many adjacent non-working days preceeded it).
    13. Skipping any additional weekends and/or non-working weekdays that may have been spanned after adding any non-working weekdays back in that may have been spanned when adding initial weekends spanned when adding # of working days alone - and repeating until no longer necessary.

    SUGGESTIONS: Of course, as with any recursive algorithm, this one can be converted to an iterative one (by implementing your own stack, i.e. with a Temp Table), but I think the 32 nesting levels is way more than enough for the vast majority of real-world use cases. Also, of course, you can make it more generic / portable by passing in the non-working weekday dates as a Table-Valued Parameter vs. a hard-coded Table reference.

    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
    
    0 讨论(0)
  • 2020-12-02 23:33

    I don't have Sql Server at the moment to test but this is the idea:

    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
    
    0 讨论(0)
提交回复
热议问题