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