Add business days to date in SQL without loops

后端 未结 25 2753
无人共我
无人共我 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:19

    Have you thought about pre-populating a look-up table that contains all of the working days (using your function) , for example WorkingDays(int DaySequenceId, Date WorkingDate), you can then use this table by selecting the DaySequenceId of the @fromDate and add @daysToAdd to get the new working date. Obviously this method also has the additional overhead of administering the WorkingDays table, but you could pre-populate it with the range of dates you expect. The other downside is the working dates that can be calculated will only be those contained within the WorkingDays table.

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

    *I know this is an old thread but found something extremely useful a while ago, modified it and got this.

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

    Update: I am sorry in a haste to find a piece of code (in a single statement) and to avoid using a function, I posted incorrect code here.

    Bit mentioned above can be used if the number of days you are adding is 7 or less.

    I have changed the code with required parameters for better understanding.

    Anyway, I ended up using what 'Nate Cook' has mentioned above. And used it as a single line of code. (Because I am restraining from using functions)

    Nate's code

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

    --Refactoring my original answer... I've added the option to define the starting point of the calculation if the starting date happens to be a weekend day: start from that weekend day or shift to the nearest weekday depending on the direction of the delta.

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

    I could not find a satisfactory solution to this that I could understand, so I ended up mostly writing one myself. This started off structurally similar to Damien_The_Unbeliever's answer but diverged quite a bit as I couldn't get it to work.

    My requirements

    • I'm using Redshift, so has to work there.
    • Negative number-of-days inputs must work (e.g. add -1 or -12 business days).
    • The output must be correct when input date is a weekday (e.g. Mon + 2 → Wed; Tue - 4 → previous Wed).
    • The solution must be documented.

    Nice-to-haves

    • Sane output for weekend days. (I chose to roll weekend days to their following Mondays, so e.g. Sunday + 1 == Monday + 1 == Tuesday.)

    Solution

    Note: My company uses Periscope Data for BI which has a C-macro-like syntax sugar to define inline text replacements it calls Snippets (see docs). Should be easily translatable to pure SQL though -- feel free to suggest an edit to my answer if you've done that translation.

    Snippet: 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)
    ))
    

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

    The question's accepted answer produces incorrect results. E.g. select @fromDate = '03-11-1983', @DaysToAdd = 3 results in 03-14-1983 while 03-16-1983 is expected.

    I posted a working solution here, but for completeness sake I will also add it here. If you are interested in the details of the two methods go visit my original answer. If not, simply copy/pasta this into your SQL project and use UTL_DateAddWorkingDays

    Note that my solution only works if DATEFIRST is set to the default value of 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
    )
    
    0 讨论(0)
提交回复
热议问题