Caynadian
Caynadian

Reputation: 779

SQL Server function to add working days to a date

I have seen a number of postings regarding how to create an SQL Server function that will add a given number of working days to a date. But, none of them calculate exactly the way I need. We have a function that is currently done in code but that I want to move to a native SQL Server function (for use by stored procedures and in queries). We give our customers 5 working days to collect cargo before we start assessing storage charges. The 5 working days exclude weekends and holidays (we have a table with the holiday dates). The trick here is that I need to get the date immediately after the 5 working days regardless of whether it is a weekend or holiday. So I need the function to return the last working date, NOT the first working day after. So, for example:

Oct 20th (Sat) plus 5 working days = Oct 26th (Fri)
Oct 21st (Sun) plus 5 working days = Oct 26th (Fri)
Oct 22nd (Mon) plus 5 working days = Oct 29th (Mon)
May 19th (Sat) plus 5 working days with May 21st a holiday = May 28th

The 5 working days is the current allotment, but this may change in the future so the number of working days needs to be a parameter. Also, the function may be used over fairly large datasets so I would prefer doing this without loops. We are running SQL Server 2008.

Edit: This is not a duplicate of "Add business days to date in SQL without loops" as they want the ending date to be a working day. I want my ending date to be whatever date immediately follows the last grace day (ie: 5 working days Mon to Fri I want the Sat date returned, NOT the following Mon).

Upvotes: 4

Views: 20483

Answers (3)

Imran Rizvi
Imran Rizvi

Reputation: 7438

I had a similar requirement earlier so sharing following function which returns new date after adding given no of days (only working days ) to given date , it also gives option to exclude saturday from weekend.

ALTER FUNCTION [dbo].[AddDaysAndWeekends](
    @StartDate DATETIME,
    @NoOfDays INT,
    @IsSatrudayHoliday bit
)

RETURNS DATETIME AS BEGIN
    while (@NoOfDays>0)
    begin
        --add 1 day
        set @StartDate = DateAdd(day,1,@StartDate)
        --skip weekends
        while (DATEPART(dw, @StartDate) = 1 or (@IsSatrudayHoliday = 1 and DATEPART(dw, @StartDate) = 7))
        begin
            set @StartDate = DateAdd(day,1,@StartDate)
        end
        set @NoOfDays = @NoOfDays-1
    end
    --declare @dateadded int = DATEDIFF(day,@BaseDate,@StartDate)
    RETURN @StartDate
END

Note: if @StartDate falls in weekend, the function above does not consider changing @StartDate to next working date.

If there's a table with holidays declared like this:

CREATE TABLE Holiday(ID INT IDENTIFY, HolidayDate Date, ...)

Then Function consider changing @StartDate to next working date like this

ALTER FUNCTION [dbo].[AddDaysAndWeekends](
    @StartDate DATETIME,
    @NoOfDays INT,
    @IsSatrudayHoliday bit
)

RETURNS DATETIME AS BEGIN
    while (@NoOfDays>0)
    begin
        --add 1 day
        set @StartDate = DateAdd(day,1,@StartDate)
        --skip weekends
        while (
            DATEPART(dw, @StartDate) = 1 or
            (@IsSatrudayHoliday = 1 and DATEPART(dw, @StartDate) = 7) or
            exists (select 1 from Holiday where HolidayDate = convert(date, @StartDate))
        )
        begin
            set @StartDate = DateAdd(day,1,@StartDate)
        end
        set @NoOfDays = @NoOfDays-1
    end
    --declare @dateadded int = DATEDIFF(day,@BaseDate,@StartDate)
    RETURN @StartDate
END

Upvotes: 2

RichardTheKiwi
RichardTheKiwi

Reputation: 107766

create table holidays (
  date date);
GO

create function dbo.findWorkDayAfter(@date datetime, @days int)
returns date as
begin
return (
  select thedate
  from (
  select thedate=dateadd(d,v.day,cast(@date as date)),
         rn=row_number() over (order by v.day)
  from (values(1),(2),(3),(4),(5),(6),(7),(8),(9),(10))v(day)
  left join holidays h on h.date = dateadd(d,v.day,cast(@date as date))
  where h.date is null and left(datename(dw,dateadd(d,v.day,cast(@date as date))),1) <> 'S'
  ) x
  where @days = rn
  )
end
GO

Unless you have long holidays, 10 days should be enough to find the 5th next working day. Increase it if you need to.

If you need a larger number of business days from a date, you can use this which will cater for a year or three.

alter function dbo.findWorkDayAfter(@date datetime, @days int)
returns date as
begin
return (
  select thedate
  from (
  select thedate=dateadd(d,v.number,cast(@date as date)),
         rn=row_number() over (order by v.number)
  from master..spt_values v
  left join holidays h on h.date = dateadd(d,v.number,cast(@date as date))
  where h.date is null and left(datename(dw,dateadd(d,v.number,cast(@date as date))),1) <> 'S'
    and v.number >= 1 and v.type='p'
  ) x
  where @days = rn
  )
end
GO

Upvotes: 3

Danimal111
Danimal111

Reputation: 2062

All Credit to Bogdan Maxim & Peter Mortensen from Count work days between two dates . This is their post, I just added holidays to the function (This assumes you have a table "tblHolidays" with a datetime field "HolDate".) For newbies, there is a test script at the end. Happy Coding!

--Changing current database to the Master database allows function to be shared by everyone.
USE MASTER
GO
--If the function already exists, drop it.
IF EXISTS
(
    SELECT *
    FROM dbo.SYSOBJECTS
    WHERE ID = OBJECT_ID(N'[dbo].[fn_WorkDays]')
    AND XType IN (N'FN', N'IF', N'TF')
)

DROP FUNCTION [dbo].[fn_WorkDays]
GO
 CREATE FUNCTION dbo.fn_WorkDays
--Presets
--Define the input parameters (OK if reversed by mistake).
(
    @StartDate DATETIME,
    @EndDate   DATETIME = NULL --@EndDate replaced by @StartDate when DEFAULTed
)

--Define the output data type.
RETURNS INT

AS
--Calculate the RETURN of the function.
BEGIN
    --Declare local variables
    --Temporarily holds @EndDate during date reversal.
    DECLARE @Swap DATETIME

    --If the Start Date is null, return a NULL and exit.
    IF @StartDate IS NULL
        RETURN NULL

    --If the End Date is null, populate with Start Date value so will have two dates (required by DATEDIFF below).
    IF @EndDate IS NULL
        SELECT @EndDate = @StartDate

    --Strip the time element from both dates (just to be safe) by converting to whole days and back to a date.
    --Usually faster than CONVERT.
    --0 is a date (01/01/1900 00:00:00.000)
    SELECT @StartDate = DATEADD(dd,DATEDIFF(dd,0,@StartDate), 0),
            @EndDate   = DATEADD(dd,DATEDIFF(dd,0,@EndDate)  , 0)

    --If the inputs are in the wrong order, reverse them.
    IF @StartDate > @EndDate
        SELECT @Swap      = @EndDate,
               @EndDate   = @StartDate,
               @StartDate = @Swap

    --Calculate and return the number of workdays using the input parameters.
    --This is the meat of the function.
    --This is really just one formula with a couple of parts that are listed on separate lines for documentation purposes.
    RETURN (
        SELECT
        --Start with total number of days including weekends
        (DATEDIFF(dd,@StartDate, @EndDate)+1)
        --Subtact 2 days for each full weekend
        -(DATEDIFF(wk,@StartDate, @EndDate)*2)
        --If StartDate is a Sunday, Subtract 1
        -(CASE WHEN DATENAME(dw, @StartDate) = 'Sunday'
            THEN 1
            ELSE 0
        END)
        --If EndDate is a Saturday, Subtract 1
        -(CASE WHEN DATENAME(dw, @EndDate) = 'Saturday'
            THEN 1
            ELSE 0
        END)
        --Subtract all holidays
        -(Select Count(*) from tblHolidays
          where HolDate between @StartDate and @EndDate )
        )
    END  
GO

/*
    -- Test Script
declare @EndDate datetime= dateadd(m,2,getdate())
print @EndDate
select  [Master].[dbo].[fn_WorkDays] (getdate(), @EndDate)
*/

Upvotes: 1

Related Questions