Related
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 return the date of the following Monday. I'm not bothered about any holidays, only weekends are excluded.
The problem is that this is currently done using a while loop, and it appears to be massively slowing down the stored procedure that uses it while generating a table. Does anyone know if there is any way to perform this calculation without while loops or cursors?
Just for information, this is the current function:
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
This is better if anyone is looking for a TSQL solution. No loops, no tables, no case statements AND works with negatives. Can anyone beat that?
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
This answer has been significantly altered since it was accepted, since the original was wrong. I'm more confident in the new query though, and it doesn't depend on DATEFIRST
I think this should cover it:
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
We split the time to be added into a number of weeks and a number of days within a week. We then use a small numbers table to work out if adding those few days will result in us hitting a Saturday. If it does, then we need to add 2 more days onto the total.
This answers is based on #ElmerMiller's answer.
It fixes the negative value on Sunday comment from #FistOfFury
Negative values don't work if the date passed in is Sunday
And the DATEFIRST setting comment from #Damien_The_Unbeliever
But this one does assume a particular DATEFIRST setting (7), which some of the others don't need.
Now the corrected function
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
Building off of the answer that was accepted for this question, the following user-defined function (UDF) should work in all cases--regardless of the setting for ##DateFirst.
UPDATE: As comments below indicate, this function is designed for the FromDate to be a weekday. The behavior is undefined when a weekend day is passed in as the 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
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.
To expand on Amine's comment and Nate cook's answer above, the one-liner solution to this is:
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))
Note you can add or subtract days to go forwards and backwards in time, respectively.
*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()))
)
I have tested all of the solutions proposed here and none of them work.
Here are some test scenarios that broke a lot of the above solutions.
(assuming Saturday and Sunday are the days you are excluding):
-Add 0 days to a Saturday - Expected result = Saturday
-Add 0 days to a Sunday - Expected result = Sunday
-Add 1 day to Friday - Expected result = the following Monday
-Add 1 day to Saturday - Expected result = the following Monday
-Add 1 day to Sunday - Expected result = the following Monday
-Add 3 days to Friday - Expected result = the following Wednesday
-Add 5 days to Saturday - Expected result = the following Friday
-Add 5 days to Friday - Expected result = the following Friday
-Subtract 1 day from Monday - Expected result = the previous Friday
-Subtract 1 day from Sunday - Expected result = the previous Friday
-Subtract 1 day from Saturday - Expected result = the previous Friday
-Subtract 3 days from Monday - Expected result = the previous Wednesday
-Subtract 5 days from Saturday - Expected result = the previous Monday
-Subtract 5 days from Monday - Expected result = the previous Monday
Here is what I wrote after reading this entire thread and picking the good pieces of logic:
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
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;
This function can add and subtract business days regardless of the value of ##DATEFIRST. To subtract business days use a negative number of days.
I found a much more elegant approach from Microsoft Docs. It takes into account skipping multiple weekends. Super clean.
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
Run the test
SELECT dbo.DAYSADDNOWK(GETDATE(), 15)
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
Thanks Damien for the code. There was a slight error in the calcs in that it added only 1 day for the sunday, and that when the number of business days crossed a weekend (but did not land in the weekend) the extra 2 days was not taken into account. Here is a modified version of Damiens code that works with the default datefirst at 7. Hope this helps.
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
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
)
This is what I use:
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
The "SET DATEFIRST 1;" part is necessary to set Monday as the first day of the week.
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)
This is an old thread but I just created a table with all the dates then did this:
SELECT Count(*)
FROM Date_Table
WHERE [day] BETWEEN #StartDate and #EndDate
AND DATENAME(weekday, [day]) NOT IN ('Sunday', 'Saturday')
I know it's a little bit late, perhaps someone else stumble upon this problem.
I've tried the above solution but, most of them can't calculate holidays.
This is how i tried
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
Where Holiday is a table with holiday_date as a reference for holiday
Hope this can help some one.
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've very recently solved this problem to add two working days to the current date by creating an INT value #DaysToAdd - tested and working great on 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
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
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:
Mnemonic identifier names.
Comments explaining code that's not clear.
Not checking every single work day needing to be incremented (i.e.
much less than O(n) complexity).
Negative work day increments.
Allowing non-12 am time portion to be passed in (so you won't have to strip it first).
Retaining the passed-in time portion, if any, in the result (in case you need the exact time x-business days ahead/ago).
Weekend day names in languages other than English.
##DateFirst values other than the default (7 aka U.S.).
Specifying a custom list of non-weekend non-working days.
Allowing list of non-weekend non-working days to work if passed-in date has a non-12 am time.
Returning starting date-time if # work days increment is 0 even if starting date-time is on a non-working day.
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).
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
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)
--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
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])
))
Very late to the party, but I stumbled upon the very same question. And though there are a myriad of answers here, I just wanted to add my solution, b/c the solutions here are:
Either not working with negative offsets
Or do not work with different ##DATEFIRST settings
Or are using loops for something which could be solved solely with modulus arithmetic.
or are overly complicated in their branching logic
Thus, here's my solution, which I validated using Excel's WORKDAY function for positive and negative offsets:
CREATE OR ALTER FUNCTION dbo.AddBusinessDays(#startdate AS DATETIME, #n AS INT)
RETURNS DATETIME
AS
BEGIN
DECLARE #result DATETIME;
DECLARE #nrweeks INT,
#nrdays INT;
DECLARE #wd TINYINT;
DECLARE #residdays SMALLINT;
SET #wd = ((DATEPART(DW, #startdate) - 1) + ##DATEFIRST) % 7;
--- 6 working days correspond to 1 full week and 1 extra day
SET #nrweeks = #n / 5;
SET #residdays = #n % 5;
/*
(1) transform working weeks into calendar weeks
(2) if residual days + starting day touches a saturday add 2 days for the weekend
(3) unless if we started on a saturday we should not count it, e.g. SAT + 1 WD would result in 1 + 2 => TUE, but it should be MON so 1 + 2 - 1
(4) if we have a full working week w/o residual days and started on the weekend remove touching condition altogether, e.g. SAT + 5 WD: 7 + 2 - 1 => SUN, but it should be FRI, so 7 + 2 - 1 - 2
(1a) - (4a) likewise but for negative logic, i.e. all logic regarding SAT should be logic regarding SUN and signs have to be switched
*/
IF #n = 0
SET #nrdays = 0;
ELSE IF #n > 0
SET #nrdays = #residdays + 7 * #nrweeks + --- (1)
IIF(#wd + #residdays >= 6, 2, 0) + --- (2)
IIF(#wd = 6, -1, 0) + --- (3)
IIF(#residdays = 0 AND #wd % 6 = 0, -2, 0); --- (4)
ELSE
SET #nrdays = #residdays + 7 * #nrweeks + --- (1a)
IIF(#wd + #residdays <= 0, -2, 0) + --- (2a)
IIF(#wd = 0, 1, 0) + --- (3a)
IIF(#residdays = 0 AND #wd % 6 = 0, 2, 0); --- (4a)
SET #result = DATEADD(DAY, #nrdays, #startdate);
RETURN #result;
END
N.B. This solution is obviously made for T-SQL.
How can I calculate the date of the upcoming second Monday in January relative to the current date using a SQL query?
Below is a query that I have which doesn't work if the current date is of the same year but prior to the upcoming second Monday of January.
declare #secondMondayOfJan date = dateadd(day , ((17 - datepart(dw,current_timestamp)) % 7) + 6, current_timestamp)
IF current_timestamp > #secondMondayOfJan
select #secondMondayOfJan as Deadline
ELSE
declare #firstDayOfNextYear date = datefromparts(year(current_timestamp)+1,1,1)
set #secondMondayOfJan = dateadd(day, ((17 - datepart(dw,#firstDayOfNextYear)) % 7) + 6, #firstDayOfNextYear)
select #secondMondayOfJan as Deadline
This looks quite verbose, but hopefully makes the logic clear, and doesn't depend on any particular settings (such as DATEFIRST or language settings):
;With Nums(n) as (
select 0 union all select 1 union all select 2 union all select 3
union all select 4 union all select 5 union all select 6
), ThisAndNext as (
select
DATEADD(year,DATEDIFF(year,'20010101',GETDATE()),'20010108') as SecondWeek,
0 as Choose
union all
select
DATEADD(year,DATEDIFF(year,'20010101',GETDATE()),'20020108'),
1
), Combined as (
select DATEADD(day,n,SecondWeek) as Possible,Choose
from ThisAndNext cross join Nums
)
select top 1 Possible from Combined
where Possible >= DATEADD(day,DATEDIFF(day,0,GETDATE()),0)
and DATEPART(weekday,Possible) = DATEPART(weekday,'20150504')
order by Choose
Nums is just a small set of numbers - it can be replaced with a select from a numbers table, if you already have one.
ThisAndNext finds the 8th of this year and next year, as two separate rows.
Combined adds between 0 and 6 days onto the 8th of this year and next.
Finally, we select the first date from Combined that is (the following bullet points correspond to the lines of the WHERE clause)
equal to or greater than today's date (using another instance of the DATEADD/DATEDIFF pattern to remove the time element from GETDATE())
on a Monday (by comparing it to an arbitrary, well known Monday)
by preference, from this year rather than next
All taken together, this means that we have selected a future (or current) day that falls between the 8th and the 14th of January that is a Monday. If you don't want "today" to be a possible result, just change the >= comparison towards the end to be a >.
The logic behind this code is:
get the first day of the current year
get the first day of the next year
find the 2nd weekday of jan of the current year
find the 2nd weekday of jan of the next year
compare the current date if is greater than or less than or equal and based on this set the deadline to the correct 2nd weekday
It's setup to retrieve the 2nd monday, but changing the value of the variable #dw between 1 to 7, you will get the others weekday
(1 = Sunday, 2 = Monday, 3 = Tuesday, 4 = Wednesday, 5 = Thursday, 6 = Friday, 7 = Saturday)
You can play with this code at:
http://rextester.com/TVKYW75798
/*
THIS STATEMENT WILL RETURN THE 2ND WEEKDAY OF JAN BASED ON CURRENT DATE.
IF CURRENT_DATE = 2ND WEEKDAY OF JAN
THEN DEADLINE = CURRENT_DATE
IF CURRENT_DATE < 2ND WEEKDAY OF JAN
THEN DEADLINE = 2ND WEEKDAY OF JAN (CURRENT YEAR)
IF CURRENT_DATE > 2ND WEEKDAY OF JAN
THEN DEADLINE = 2ND WEEKDAY OF JAN (NEXT YEAR)
YOU CAN PLAY WITH VARIABLE #date and #dw TO TEST.
I LEAVE COMMENTED IN THE CODE FOUR SCENARIOS (YEAR 2015)
WHERE YOU WILL BE ABLE TO CHECK THE CONDITIONS ABOVE.
2015-01-12 WAS THE 2ND MONDAY OF JAN
*/
DECLARE #date DATE = GETDATE()
-- SET #date = '2015-01-01'
-- SET #date = '2015-01-11'
-- SET #date = '2015-01-12'
-- SET #date = '2015-01-13'
DECLARE #currentYearFirstDay DATE = CAST(datepart(yy, #date) AS VARCHAR) + '-01-01'
DECLARE #nextYearFirstDay DATE = DATEADD(yy, 1, #currentYearFirstDay)
DECLARE #d DATE
DECLARE #secondWeekdayCurrentYear DATE
DECLARE #secondWeekdayNextYear DATE
DECLARE #dw INT = 2 /*USE VALUES FROM 1 TO 7, WHERE 1 = SUNDAY AND 7 = SATURDAY*/
DECLARE #weekOfYear INT = DATEPART(ww,#d)
DECLARE #weekDay INT = DATEPART(dw,#d)
DECLARE #checkNextYear INT = 0
SET #d = #currentYearFirstDay
GOTO GET_2nd_WEEKDAY_FROM_FIRST_DAY_OF_YEAR
GET_2nd_WEEKDAY_FROM_FIRST_DAY_OF_YEAR:
BEGIN
SET #weekDay = DATEPART(dw, #d)
/*FIRST DAY OF YEAR GREATER THAN WEEKDAY, THEN 2nd WEEKDAY WILL BE THE 3rd WEEK*/
IF #weekDay > #dw
BEGIN
SET #d = DATEADD(ww, 2, #d)
END
/*FIRST DAY OF YEAR LESS THAN OR EQUAL WEEKDAY, THEN 2nd WEEKDAY WILL BE THE 2nd WEEK*/
ELSE
BEGIN
SET #d = DATEADD(ww, 1, #d)
END
IF #checkNextYear = 0
BEGIN
SET #secondWeekdayCurrentYear = DATEADD(dw, -(#weekDay - #dw), #d)
SET #d = #nextYearFirstDay
SET #checkNextYear = 1
GOTO GET_2nd_WEEKDAY_FROM_FIRST_DAY_OF_YEAR
END
ELSE
BEGIN
SET #secondWeekdayNextYear = DATEADD(dd, -(#weekDay - #dw), #d)
GOTO EXIT_BLOCK
END
END
EXIT_BLOCK:
SELECT CASE
WHEN #date > #secondWeekdayCurrentYear
THEN #secondWeekdayNextYear
ELSE
CASE
WHEN #date = #secondWeekdayCurrentYear
THEN #date
ELSE #secondWeekdayCurrentYear
END
END AS DEADLINE
For example:
#dtBegin = '2012-06-29'
#input = 20
I want the output to be '2012-07-27'.
I had to tackle the same problem in my project. Gordon Lionoff's solution got me on the right track but did not always produce the right result. I also have to take in account dates that start in a weekend. I.e. adding 1 business day to a saturday or sunday should result in a monday. This is the most common approach to handling business day calculation.
I've created my own solution based on Gordon Linoff's function and Patrick McDonald's excellent C# equivalent
NOTE: My solution only works if DATEFIRST is set to the default value of 7. If you use a different DATEFIRST value, you will have to change the 1, 7 and (1,7,8,9,10) bits.
My solution consists of two functions. An "outer" function that handles edge cases and an "inner" function that performs the actual calculation. Both functions are table-valued functions so they will actually be expanded into the implementing query and fed through the query optimizer.
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
)
As you can see, the "outer" function handles:
When adding no days, return the original date. This will keep saturday and sunday dates intact.
When adding days to a sunday, start counting from monday. This consumes 1 day.
When adding days to a saturday, start counting from monday. This consumes 1 day.
In all other cases, perform the usual calculation
_
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
)
The "inner" function is similar to Gordon Linoff's solution, except it accounts for dates crossing weekend boundaries but without crossing a full week boundary.
Finally, I created a test script to test my function. The expected values were generated using Patrick McDonald's excellent C# equivalent and I randomly cross-referenced this data with this popular calculator.
You can do this without resorting to a calendar table or user defined function:
dateadd(d,
(#input / 5) * 7 + -- get complete weeks out of the way
mod(#input, 5) + -- extra days
(case when ((#input%5) + datepart(dw, #dtbegin)%7) in (7, 1, 8) or
((#input%5) + datepart(dw, #dtbegin)%7) < (#input%5)
then 2
else 0
end),
#dtbegin
)
I'm not saying this is pretty. But sometimes arithmetic is preferable to a join or a loop.
This is what I've tried:
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
Where Holiday is a table with holiday_date as a reference for holiday.
I realize I'm about 8 years late to this party... But I took Martin's answer and updated it to:
1. a single scalar function instead of 2 nested table-valued functions
I tested using his original test script and my function passes too. Also, it seems the rebuild to a scalar function has a slight positive impact on performance. Both versions seem to benefit equally from 'buffer caching', the scalar version performing up to 25% better non-cached and up to 40% better cached. Disclaimer: I just ran both versions a bunch of times and recorded the times, I did not do any decent performance testing.
2. include support for DATEFIRST is either Monday, Saturday or Sunday
I feel a UDF should be agnostic to the datefirst setting. I'm in Europe and Monday is the default here. The original function would not work without adaptation.
According to wikipedia Monday, Saturday and Sunday are the only real world first days of the week. Support for other could easily be added, but would make the code more bulky and I have a hard time envisioning a real world use case.
CREATE FUNCTION dbo.fn_addWorkDays
(
#date datetime,
#days int
)
RETURNS DATETIME
AS
BEGIN
IF NOT ##DATEFIRST IN (1,6,7) BEGIN --UNSUPPORTED DATE FIRST
RETURN NULL
/* MONDAY = FRIST DAY */
END ELSE IF #days = 0 BEGIN
RETURN #date
END ELSE IF ##DATEFIRST = 1 AND DATEPART(dw, #date) = 7 BEGIN --SUNDAY
SET #date = DATEADD(d, 1, #date)
SET #days = #days - 1
END ELSE IF ##DATEFIRST = 1 AND DATEPART(dw, #date) = 6 BEGIN --SATURDAY
SET #date = DATEADD(d, 2, #date)
SET #days = #days - 1
/* SATURDAY = FRIST DAY */
END ELSE IF ##DATEFIRST = 7 AND DATEPART(dw, #date) = 2 BEGIN --SUNDAY
SET #date = DATEADD(d, 1, #date)
SET #days = #days - 1
END ELSE IF ##DATEFIRST = 7 AND DATEPART(dw, #date) = 1 BEGIN --SATURDAY
SET #date = DATEADD(d, 2, #date)
SET #days = #days - 1
/* SUNDAY = FRIST DAY */
END ELSE IF ##DATEFIRST = 7 AND DATEPART(dw, #date) = 1 BEGIN --SUNDAY
SET #date = DATEADD(d, 1, #date)
SET #days = #days - 1
END ELSE IF ##DATEFIRST = 7 AND DATEPART(dw, #date) = 7 BEGIN --SATURDAY
SET #date = DATEADD(d, 2, #date)
SET #days = #days - 1
END
DECLARE #return AS dateTime
SELECT #return =
DATEADD(d
, (#days / 5) * 7
+ (#days % 5)
+ (CASE
/* MONDAY = FRIST DAY */
WHEN ##DATEFIRST = 1 AND ((#days%5) + DATEPART(dw, #date)) IN (6,7,8,9) THEN 2
/* SATURDAY = FRIST DAY */
WHEN ##DATEFIRST = 7 AND ((#days%5) + DATEPART(dw, #date)) IN (1,2,8,9,10) THEN 2
/* SUNDAY = FRIST DAY */
WHEN ##DATEFIRST = 7 AND ((#days%5) + DATEPART(dw, #date)) IN (1,7,8,9,10,11) THEN 2
ELSE 0
END)
, #date)
RETURN #return
END
I hope this might benefit someone!
you can use the below mentioned code for exclude weekends
go
if object_id('UTL_DateAddWorkingDays') is not null
drop function UTL_DateAddWorkingDays
go
create FUNCTION [dbo].[UTL_DateAddWorkingDays]
(
#date datetime,
#daysToAdd int
)
RETURNS date
as
begin
declare #daysToAddCnt int=0,
#Dt datetime
declare #OutTable table
(
id int identity(1,1),
WeekDate date,
DayId int
)
while #daysToAdd>0
begin
set #Dt=dateadd(day,#daysToAddCnt,#date)
--select #daysToAddCnt cnt,#Dt date,DATEPART(weekday,#Dt) dayId,#daysToAdd daystoAdd
if(DATEPART(weekday,#Dt) <>7 and DATEPART(weekday,#Dt)<>1)
begin
insert into #outTable (WeekDate,DayId)
select #Dt,DATEPART(weekday,DATEADD(day,#daysToAddCnt,#Dt))
set #daysToAdd=#daysToAdd-1
end
set #daysToAddCnt=#daysToAddCnt+1
end
select #Dt=max(WeekDate) from #outTable
return #Dt
end
what about this?
declare #d1 date='2012-06-29'
declare #days int=20
select dateadd(dd,#days,#d1)
select dateadd(dd,case DATEPART(dw,t1.d) when 6 then +2 when 7 then +1 else +0 end,t1.d)
from
(
select dateadd(dd,CEILING((convert(float,#days)/5)*2)+#days,#d1)d
)t1
i found how many we in the range by CEILING((convert(float,#days)/5)*2)
then i added them to date and at the end i check if is a saturday or sunday and i add 1 or 2 days.
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 return the date of the following Monday. I'm not bothered about any holidays, only weekends are excluded.
The problem is that this is currently done using a while loop, and it appears to be massively slowing down the stored procedure that uses it while generating a table. Does anyone know if there is any way to perform this calculation without while loops or cursors?
Just for information, this is the current function:
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
This is better if anyone is looking for a TSQL solution. No loops, no tables, no case statements AND works with negatives. Can anyone beat that?
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
This answer has been significantly altered since it was accepted, since the original was wrong. I'm more confident in the new query though, and it doesn't depend on DATEFIRST
I think this should cover it:
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
We split the time to be added into a number of weeks and a number of days within a week. We then use a small numbers table to work out if adding those few days will result in us hitting a Saturday. If it does, then we need to add 2 more days onto the total.
This answers is based on #ElmerMiller's answer.
It fixes the negative value on Sunday comment from #FistOfFury
Negative values don't work if the date passed in is Sunday
And the DATEFIRST setting comment from #Damien_The_Unbeliever
But this one does assume a particular DATEFIRST setting (7), which some of the others don't need.
Now the corrected function
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
Building off of the answer that was accepted for this question, the following user-defined function (UDF) should work in all cases--regardless of the setting for ##DateFirst.
UPDATE: As comments below indicate, this function is designed for the FromDate to be a weekday. The behavior is undefined when a weekend day is passed in as the 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
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.
To expand on Amine's comment and Nate cook's answer above, the one-liner solution to this is:
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))
Note you can add or subtract days to go forwards and backwards in time, respectively.
*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()))
)
I have tested all of the solutions proposed here and none of them work.
Here are some test scenarios that broke a lot of the above solutions.
(assuming Saturday and Sunday are the days you are excluding):
-Add 0 days to a Saturday - Expected result = Saturday
-Add 0 days to a Sunday - Expected result = Sunday
-Add 1 day to Friday - Expected result = the following Monday
-Add 1 day to Saturday - Expected result = the following Monday
-Add 1 day to Sunday - Expected result = the following Monday
-Add 3 days to Friday - Expected result = the following Wednesday
-Add 5 days to Saturday - Expected result = the following Friday
-Add 5 days to Friday - Expected result = the following Friday
-Subtract 1 day from Monday - Expected result = the previous Friday
-Subtract 1 day from Sunday - Expected result = the previous Friday
-Subtract 1 day from Saturday - Expected result = the previous Friday
-Subtract 3 days from Monday - Expected result = the previous Wednesday
-Subtract 5 days from Saturday - Expected result = the previous Monday
-Subtract 5 days from Monday - Expected result = the previous Monday
Here is what I wrote after reading this entire thread and picking the good pieces of logic:
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
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;
This function can add and subtract business days regardless of the value of ##DATEFIRST. To subtract business days use a negative number of days.
I found a much more elegant approach from Microsoft Docs. It takes into account skipping multiple weekends. Super clean.
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
Run the test
SELECT dbo.DAYSADDNOWK(GETDATE(), 15)
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
Thanks Damien for the code. There was a slight error in the calcs in that it added only 1 day for the sunday, and that when the number of business days crossed a weekend (but did not land in the weekend) the extra 2 days was not taken into account. Here is a modified version of Damiens code that works with the default datefirst at 7. Hope this helps.
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
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
)
This is what I use:
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
The "SET DATEFIRST 1;" part is necessary to set Monday as the first day of the week.
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)
This is an old thread but I just created a table with all the dates then did this:
SELECT Count(*)
FROM Date_Table
WHERE [day] BETWEEN #StartDate and #EndDate
AND DATENAME(weekday, [day]) NOT IN ('Sunday', 'Saturday')
I know it's a little bit late, perhaps someone else stumble upon this problem.
I've tried the above solution but, most of them can't calculate holidays.
This is how i tried
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
Where Holiday is a table with holiday_date as a reference for holiday
Hope this can help some one.
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've very recently solved this problem to add two working days to the current date by creating an INT value #DaysToAdd - tested and working great on 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
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
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:
Mnemonic identifier names.
Comments explaining code that's not clear.
Not checking every single work day needing to be incremented (i.e.
much less than O(n) complexity).
Negative work day increments.
Allowing non-12 am time portion to be passed in (so you won't have to strip it first).
Retaining the passed-in time portion, if any, in the result (in case you need the exact time x-business days ahead/ago).
Weekend day names in languages other than English.
##DateFirst values other than the default (7 aka U.S.).
Specifying a custom list of non-weekend non-working days.
Allowing list of non-weekend non-working days to work if passed-in date has a non-12 am time.
Returning starting date-time if # work days increment is 0 even if starting date-time is on a non-working day.
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).
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
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)
--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
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])
))
Very late to the party, but I stumbled upon the very same question. And though there are a myriad of answers here, I just wanted to add my solution, b/c the solutions here are:
Either not working with negative offsets
Or do not work with different ##DATEFIRST settings
Or are using loops for something which could be solved solely with modulus arithmetic.
or are overly complicated in their branching logic
Thus, here's my solution, which I validated using Excel's WORKDAY function for positive and negative offsets:
CREATE OR ALTER FUNCTION dbo.AddBusinessDays(#startdate AS DATETIME, #n AS INT)
RETURNS DATETIME
AS
BEGIN
DECLARE #result DATETIME;
DECLARE #nrweeks INT,
#nrdays INT;
DECLARE #wd TINYINT;
DECLARE #residdays SMALLINT;
SET #wd = ((DATEPART(DW, #startdate) - 1) + ##DATEFIRST) % 7;
--- 6 working days correspond to 1 full week and 1 extra day
SET #nrweeks = #n / 5;
SET #residdays = #n % 5;
/*
(1) transform working weeks into calendar weeks
(2) if residual days + starting day touches a saturday add 2 days for the weekend
(3) unless if we started on a saturday we should not count it, e.g. SAT + 1 WD would result in 1 + 2 => TUE, but it should be MON so 1 + 2 - 1
(4) if we have a full working week w/o residual days and started on the weekend remove touching condition altogether, e.g. SAT + 5 WD: 7 + 2 - 1 => SUN, but it should be FRI, so 7 + 2 - 1 - 2
(1a) - (4a) likewise but for negative logic, i.e. all logic regarding SAT should be logic regarding SUN and signs have to be switched
*/
IF #n = 0
SET #nrdays = 0;
ELSE IF #n > 0
SET #nrdays = #residdays + 7 * #nrweeks + --- (1)
IIF(#wd + #residdays >= 6, 2, 0) + --- (2)
IIF(#wd = 6, -1, 0) + --- (3)
IIF(#residdays = 0 AND #wd % 6 = 0, -2, 0); --- (4)
ELSE
SET #nrdays = #residdays + 7 * #nrweeks + --- (1a)
IIF(#wd + #residdays <= 0, -2, 0) + --- (2a)
IIF(#wd = 0, 1, 0) + --- (3a)
IIF(#residdays = 0 AND #wd % 6 = 0, 2, 0); --- (4a)
SET #result = DATEADD(DAY, #nrdays, #startdate);
RETURN #result;
END
N.B. This solution is obviously made for T-SQL.
I seem to be asking a lot of SQL questions at the moment. (I would normally write a program to sort the data into a report table, however at the moment that is not possible, and needs to be done using SQL)
The Question
I need a where clause that returns results with the next working day. i.e. Monday 01/01/01 the next working day would be Tuesday 02/01/01 which could be achieved with a simple date add. However on a Friday 05/01/01 the next working day is Monday 08/01/01. Is there anything built in to help cope with this easily?
Thanks for your advice.
The key is to use the DATEPART(weekday,#date) function, it'll return the day of the week, so if it's saturday or sunday you just add one or two to the current date to get the desired result.
You can create a user defined function to do so easily, for example Pinal Dave has this
CREATE FUNCTION dbo.udf_GetPrevNextWorkDay (#dtDate DATETIME, #strPrevNext VARCHAR(10))
RETURNS DATETIME
AS
BEGIN
DECLARE #intDay INT
DECLARE #rtResult DATETIME
SET #intDay = DATEPART(weekday,#dtDate)
--To find Previous working day
IF #strPrevNext = 'Previous'
IF #intDay = 1
SET #rtResult = DATEADD(d,-2,#dtDate)
ELSE
IF #intDay = 2
SET #rtResult = DATEADD(d,-3,#dtDate)
ELSE
SET #rtResult = DATEADD(d,-1,#dtDate)
--To find Next working day
ELSE
IF #strPrevNext = 'Next'
IF #intDay = 6
SET #rtResult = DATEADD(d,3,#dtDate)
ELSE
IF #intDay = 7
SET #rtResult = DATEADD(d,2,#dtDate)
ELSE
SET #rtResult = DATEADD(d,1,#dtDate)
--Default case returns date passed to function
ELSE
SET #rtResult = #dtDate
RETURN #rtResult
END
GO
CREATE FUNCTION dbo.uf_GetNextWorkingDay (#givenDate DATETIME)
RETURNS DATETIME
AS
BEGIN
DECLARE #workingDate DATETIME
IF (DATENAME(dw , #givenDate) = 'Friday')
BEGIN
SET #workingDate = DATEADD(day, 3, #givenDate)
END
ELSE IF (DATENAME(dw , #givenDate) = 'Saturday')
BEGIN
SET #workingDate = DATEADD(day, 2, #givenDate)
END
ELSE
BEGIN
SET #workingDate = DATEADD(day, 1, #givenDate)
END
RETURN #workingDate
END
A good article http://ryanfarley.com/blog/archive/2005/02/14/1685.aspx
You could do this with a simple day check and add 3 days rather than one if its a Friday. However, do you need to take into consideration public holidays though?
All Credit to Cashif -- I modified his to include a Holiday table (tblHolidays with a date field HolDate) which is very lightweight -- about 10 rows a year if you're lucky enough to get that many days off!
My version returns date type (which I find easier to work with). I also run thru this more than once -- if Thursday is your startday and Friday is a holiday, you have to again add 2 more days to get to Monday (or the next working day). Then it checkes to make sure the next week doesn't start with a holiday.
CREATE FUNCTION [dbo].[GetNextWorkingDay] (#givenDate DATE)
RETURNS DATE
AS
BEGIN
DECLARE #workingDate DATETIME
IF (DATENAME(dw , #givenDate) = 'Friday')
BEGIN
SET #workingDate = DATEADD(day, 3, #givenDate)
END
ELSE IF (DATENAME(dw , #givenDate) = 'Saturday')
BEGIN
SET #workingDate = DATEADD(day, 2, #givenDate)
END
ELSE
BEGIN
SET #workingDate = DATEADD(day, 1, #givenDate)
END
while ((Select count(*) from tblHolidays where holdate = #workingDate) > 0)
begin
set #workingDate = dateadd(dd,1,#WorkingDate)
end
-- if adding a day makes it a Saturday, add 2 more to get to Monday (and test to make sure the week doesn't start with a holiday)
IF (DATENAME(dw , #workingDate) = 'Saturday')
BEGIN
SET #workingDate = DATEADD(day, 2, #workingDate)
END
while ((Select count(*) from tblHolidays where holdate = #workingDate) > 0)
begin
set #workingDate = dateadd(dd,1,#WorkingDate)
end
RETURN #workingDate
END
... of course you should refactor so the code doesn't repeat, and include a while clause to repeat only as many times as needed to get to a working day, but I have a deadline... that'll be for another less hectic day.
Here's what I did for a one-off application:
WHILE EXISTS (SELECT * FROM Holidays WHERE CONVERT(VARCHAR, HolidayDate,101) = CONVERT(VARCHAR,#DateVariable 101 ) OR DATENAME(WEEKDAY, #DateVariable)='SATURDAY' OR DATENAME(WEEKDAY, #DateVariable)='SUNDAY')
BEGIN
SET #DateVariable = DateAdd(day,1,#DateVariable)
PRINT #DateVariable -- If you want
END
Note that, our Holidays table stores all holidays for the past and future years.
You could do this with a simple case statement.
select case when datepart(dw, getdate()) >= 6 then getdate() + (9 - datepart(dw, getdate())) else getdate() + 1 end
What you need is a calendar table. Getting the next working days is not so simple if you need to account for holidays other than the weekend. The table basically contains just two columns Date and an integer field indicating whether it is a working/non working day- but there can be other columns for - example quarter etc as well.
This table is populated once a year and then maintained as necessary. Getting the result for next working day then becomes as simple as a query like this.
SELECT field1,filed2 from your table T where your date_Field = (SELECT min(date) From calendar table where WorkingDay = 1 and date > GetDate())
/P
How about somethin like this?
select * from table
where (date = dateadd(dd,1,#today) and datepart(weekday,#today) not in (6,0) ) --its not friday or saturday
or (date = dateadd(dd,2,#today) and datepart(weekday,#today) = 0) -- its saturday
or (date = dateadd(dd,3,#today) and datepart(weekday,#today) = 6) --its friday
the date attribute should have the same time as #today or else you have to also use between
It's been basically answered above, but it took me a while to get it and I thought maybe this will help somebody. Is used a simple CASE-WHEN.
This is a check in order to find out which days are weekdays
DATEPART(dw, date) -> MON through FRI = 1,2,3,4,5 SAT = 6, SUN = 7
This is the CASE-WHEN:
CASE
WHEN DATEPART(weekday, date) <= 5 THEN date -- weekdays, no change
WHEN DATEPART(weekday, date) = 6 THEN date + 2 -- SAT + 2 = MON
WHEN DATEPART(weekday, date) = 6 THEN date + 1 -- SUN + 1 = MON
END AS 'WEEKDAY_DATES'
Check, if the statement actually gets you weekdays only:
CASE
WHEN DATEPART(weekday, date) <= 5 THEN DATENAME(weekday, date)
WHEN DATEPART(weekday, date) = 6 THEN DATENAME(weekday, date + 2)
WHEN DATEPART(weekday, date) = 6 THEN DATENAME(weekday, date + 1)
END AS 'WEEKDAY_NAMES'