Calculating due date using business hours and holidays - sql

I need to calculate due date / end date for SLAs. As input values I have the start date and a timespan (in minutes). This calculation needs to take into account business hours, weekends, and holidays.
I've seen a lot of examples where the input is start date and end date, but have been struggling finding anything similar to the above input values.
Is there an elegant solution to this problem? Is there a way to calculate due date without using a loop? I can't think of a way to do the calculation without doing something similar to the following terrible algorithm:
Create a return variable "due date" and set it to input variable
"start date"
Create a control variable "used minutes" and set it to 0
Create a loop with the condition "used minutes" <= "input timespan"
Inside the loop, add a second to the "due date" return variable
Inside the loop, check if the second is within hours of operation
(checking business hours, weekends, and holidays). If so, increment
control variable "used minutes" by 1.
Upon exiting the loop, return variable "due date"

You need a table with valid business hours, with the weekends and holidays excluded (or marked as weekend/holiday so you can skip them.) Each row represents one day and the number of working hours for that day. Then you query the business hours table from your start date to the first (min) date where the sum(hours*60) is greater than your minutes parameter, excluding marked weekend/holiday rows. That gives you your end date.
Here's the day table:
CREATE TABLE [dbo].[tblDay](
[dt] [datetime] NOT NULL,
[dayOfWk] [int] NULL,
[dayOfWkInMo] [int] NULL,
[isWeekend] [bit] NOT NULL,
[holidayID] [int] NULL,
[workingDayCount] [int] NULL,
CONSTRAINT [PK_tblDay] PRIMARY KEY CLUSTERED
(
[dt] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
here's how I populate the table with days:
CREATE PROCEDURE [dbo].[usp_tblDay]
AS
BEGIN
SET NOCOUNT ON;
DECLARE
#Dt datetime ,
#wkInMo int,
#firstDwOfMo int,
#holID int,
#workDayCount int,
#weekday int,
#month int,
#day int,
#isWkEnd bit
set #workDayCount = 0
SET #Dt = CONVERT( datetime, '2008-01-01' )
while #dt < '2020-01-01'
begin
delete from tblDay where dt = #dt
set #weekday = datepart( weekday, #Dt )
set #month = datepart(month,#dt)
set #day = datepart(day,#dt)
if #day = 1 -- 1st of mo
begin
set #wkInMo = 1
set #firstDwOfMo = #weekday
end
if ((#weekday = 7) or (#weekday = 1))
set #isWkEnd = 1
else
set #isWkEnd = 0
if #isWkEnd = 0 and (#month = 1 and #day = 1)
set #holID=1 -- new years on workday
else if #weekday= 6 and (#month = 12 and #day = 31)
set #holID=1 -- holiday on sat, change to fri
else if #weekday= 2 and (#month = 1 and #day = 2)
set #holID=1 -- holiday on sun, change to mon
else if #wkInMo = 3 and #weekday= 2 and #month = 1
set #holID = 2 -- mlk
else if #wkInMo = 3 and #weekday= 2 and #month = 2
set #holID = 3 -- President’s
else if #wkInMo = 4 and #weekday= 2 and #month = 5 and datepart(month,#dt+7) = 6
set #holID = 4 -- memorial on 4th mon, no 5th
else if #wkInMo = 5 and #weekday= 2 and #month = 5
set #holID = 4 -- memorial on 5th mon
else if #isWkEnd = 0 and (#month = 7 and #day = 4)
set #holID=5 -- July 4 on workday
else if #weekday= 6 and (#month = 7 and #day = 3)
set #holID=5 -- holiday on sat, change to fri
else if #weekday= 2 and (#month = 7 and #day = 5)
set #holID=5 -- holiday on sun, change to mon
else if #wkInMo = 1 and #weekday= 2 and #month = 9
set #holID = 6 -- Labor
else if #isWkEnd = 0 and (#month = 11 and #day = 11)
set #holID=7 -- Vets day on workday
else if #weekday= 6 and (#month = 11 and #day = 10)
set #holID=7 -- holiday on sat, change to fri
else if #weekday= 2 and (#month = 11 and #day = 12)
set #holID=7 -- holiday on sun, change to mon
else if #wkInMo = 4 and #weekday= 5 and #month = 11
set #holID = 8 -- thx
else if #holID = 8
set #holID = 9 -- dy after thx
else if #isWkEnd = 0 and (#month = 12 and #day = 25)
set #holID=10 -- xmas day on workday
else if #weekday= 6 and (#month = 12 and #day = 24)
set #holID=10 -- holiday on sat, change to fri
else if #weekday= 2 and (#month = 12 and #day = 26)
set #holID=10 -- holiday on sun, change to mon
else
set #holID = null
insert into tblDay select #dt,#weekday,#wkInMo,#isWkEnd,#holID,#workDayCount
if #isWkEnd=0 and #holID is null
set #workDayCount = #workDayCount + 1
set #dt = #dt + 1
if datepart( weekday, #Dt ) = #firstDwOfMo
set #wkInMo = #wkInMo + 1
end
END
I also have a holiday table, but everyone's holidays are different:
holidayID holiday rule description
1 New Year's Day Jan. 1
2 Martin Luther King Day third Mon. in Jan.
3 Presidents' Day third Mon. in Feb.
4 Memorial Day last Mon. in May
5 Independence Day 4-Jul
6 Labor Day first Mon. in Sept
7 Veterans' Day Nov. 11
8 Thanksgiving fourth Thurs. in Nov.
9 Fri after Thanksgiving Friday after Thanksgiving
10 Christmas Day Dec. 25
HTH

This is the best I could do, still uses a loop but uses date functions instead of incrementing a minutes variable. Hope you like it.
--set up our source data
declare #business_hours table
(
work_day varchar(10),
open_time varchar(8),
close_time varchar(8)
)
insert into #business_hours values ('Monday', '08:30:00', '17:00:00')
insert into #business_hours values ('Tuesday', '08:30:00', '17:00:00')
insert into #business_hours values ('Wednesday', '08:30:00', '17:00:00')
insert into #business_hours values ('Thursday', '08:30:00', '17:00:00')
insert into #business_hours values ('Friday', '08:30:00', '18:00:00')
insert into #business_hours values ('Saturday', '09:00:00', '14:00:00')
declare #holidays table
(
holiday varchar(10)
)
insert into #holidays values ('2015-01-01')
insert into #holidays values ('2015-01-02')
--Im going to assume the SLA of 2 standard business days (0900-1700) = 8*60*2 = 960
declare #start_date datetime = '2014-12-31 16:12:47'
declare #time_span int = 960-- time till due in minutes
declare #true bit = 'true'
declare #false bit = 'false'
declare #due_date datetime --our output
--other variables
declare #date_string varchar(10)
declare #today_closing datetime
declare #is_workday bit = #true
declare #is_holiday bit = #false
--Given our timespan is in minutes, lets also assume we dont care about seconds in start or due dates
set #start_date = DATEADD(ss,datepart(ss,#start_date)*-1,#start_date)
while (#time_span > 0)
begin
set #due_date = DATEADD(MINUTE,#time_span,#start_date)
set #date_string = FORMAT(DATEADD(dd, 0, DATEDIFF(dd, 0, #start_date)),'yyyy-MM-dd')
set #today_closing = (select convert(datetime,#date_string + ' ' + close_time) from #business_hours where work_day = DATENAME(weekday,#start_date))
if exists((select work_day from #business_hours where work_day = DATENAME(weekday,#start_date)))
set #is_workday = #true
else
set #is_workday = #false
if exists(select holiday from #holidays where holiday = #date_string)
set #is_holiday = #true
else
set #is_holiday = #false
if #is_workday = #true and #is_holiday = #false
begin
if #due_date > #today_closing
set #time_span = #time_span - datediff(MINUTE, #start_date, #today_closing)
else
set #time_span = #time_span - datediff(minute, #start_date, #due_date)
end
set #date_string = FORMAT(DATEADD(dd, 1, DATEDIFF(dd, 0, #start_date)),'yyyy-MM-dd')
set #start_date = CONVERT(datetime, #date_string + ' ' + isnull((select open_time from #business_hours where work_day = DATENAME(weekday,convert(datetime,#date_string))),''))
end
select #due_date

Sql to Calculate due date excluding holidays and considering business hour as below :- < note : - It's working correctly Business hour (8-5) Maintain holiday table
CREATE TABLE [dbo].[holiday](
[id] [int] IDENTITY(1,1) NOT NULL,
[region] [nvarchar](10) NULL,
[Hdate] [date] NULL,
)
>
declare #start datetime= getdate()
declare #slamins int =960 --- SLA time in mins
declare #Country varchar(2)='NA'
declare #start_hour int = 8 -- business start hour
declare #end_hour int = 17 -- business end hour
declare #true bit = 'true'
declare #false bit = 'false'
declare #is_workday bit = #true
declare #is_holiday bit = #false
declare #due_date datetime
declare #today_closing datetime
declare #temp int = 0
declare #holidays table (HDate DateTime) -- Table variable to hold holidayes
---- Get country holidays from table based on the country code (Feel free to remove this or modify as per your DB schema)
Insert Into #Holidays (HDate) Select date from HOLIDAY Where region=#Country and Hdate>=DateAdd(dd, DateDiff(dd,0,#start), 0)
--check for weekends
set #start = case(datepart(dw,#start)+##datefirst-1)%7
when 0 then Convert(dateTime,CONVERT(varchar,#start+1,101)+' 08:00:00')
when 6 then Convert(dateTime,CONVERT(varchar,#start+2,101)+' 08:00:00')
else #start end
-- check if start time is before business hour
if datepart(hh, #start) < #start_hour set #start = Convert(dateTime,CONVERT(varchar,#start,101)+' 08:00:00')
-- check if start time is after business hour
if datepart(hh, #start) >= #end_hour set #start = Convert(dateTime,CONVERT(varchar,#start+1,101)+' 08:00:00')
-- loop start
while (#slamins > 0)
begin
-- prepared closing date time based on start date
set #today_closing = Convert(dateTime,CONVERT(varchar,#start,101)+' 17:00:00')
set #due_date = #start
-- calculate number of Minute between start date and closing date
set #temp = DATEDIFF(N, #start , #today_closing);
--check for weekends
if (DATEPART(dw, #start)!=1 AND DATEPART(dw, #start)!=7)
set #is_workday = #true
else
set #is_workday = #false
--check for holidays
if (Select Count(*) From #Holidays Where HDATE=DateAdd(dd, DateDiff(dd,0,#start), 0)) = 0
set #is_holiday =#false
else
set #is_holiday = #true
if #is_workday = #true and #is_holiday = #false
begin
if(#temp < #slamins)
begin
set #slamins = #slamins - #temp
end
else
begin
set #due_date = DATEADD(MINUTE,#slamins,#start)
set #slamins = 0
print #due_date
end
end
set #start = Convert(dateTime,CONVERT(varchar,#start+1,101)+' 08:00:00')
end
select #due_date

Here is an option using a WorkSchedule table, which will contain the business hours that are available to count towards the SLA. To account for weekends and holidays, just do not insert records for these days into the WorkSchedule table.
This solution also uses a "Tally" table aka numbers table in the due date calc. I also included debug output to help you see what is going on, so just comment out or uncomment any debug sections to see less/more info.
I used SQL temp tables in this example so that you can run it without messing up your current database schema, but you should replace with physical tables if you use this solution.
Test Data setups:
CREATE TABLE #WorkSchedule(WorkStart datetime not null primary key, WorkEnd datetime not null);
GO
CREATE TABLE #Tally (N int not null primary key);
GO
--POPULATE TEST DATA
--populate Tally table
insert into #Tally (N)
select top 10000 N = row_number() over(order by o.object_id)
from sys.objects o cross apply sys.objects o2
;
go
--POPULATE WITH DUMMY TEST DATA
INSERT INTO #WorkSchedule(WorkStart, WorkEnd)
SELECT
workStart = dateadd(hour, 8, t.workDate)
, workEnd = dateadd(hour, 17, t.workDate)
FROM (
SELECT top 10000 workDate = dateadd(day, row_number() over(order by o.object_id), '2000-01-01')
FROM sys.objects o cross apply sys.objects o2
) t
--Exclude weekends from work schedule
WHERE datename(weekday, t.workDate) not in ('Saturday','Sunday')
;
GO
Code to calculate Due Date:
SET NOCOUNT ON;
DECLARE #startDate datetime;
DECLARE #SLA_timespan_mins int;
DECLARE #workStartDayOne datetime;
DECLARE #SLA_Adjusted int;
DECLARE #dueDate datetime;
--SET PARAM VALUES HERE FOR TESTING TO ANY DATE/SLA TIMESPAN YOU WANT:
SET #startDate = '2014-01-04 05:00'; --Saturday
SET #SLA_timespan_mins = 10 * 60 ; --10 hrs.
--get the info day 1, since your start date might be after the work start time.
select top 1 #workStartDayOne = s.WorkStart
--increase the SLA timespan mins to account for difference between work start and start time
, #SLA_Adjusted = case when #startDate > s.WorkStart then datediff(minute, s.WorkStart, #startDate) else 0 end + #SLA_timespan_mins
from #WorkSchedule s
where s.WorkEnd > #startDate
and s.WorkStart <> s.WorkEnd
order by s.WorkStart asc
;
--DEBUG info:
select 'Debug Info' as DebugInfo, #startDate AS StartDate, #workStartDayOne as workStartDayOne, #SLA_timespan_mins as SLA_timespan_mins, #SLA_Adjusted as SLA_Adjusted;
--now sum all the non work hours during that period and determine the additional mins that need added.
;with cteWorkMins as
(
SELECT TOP (#SLA_Adjusted)
s.WorkStart, s.WorkEnd
, WorkMinute = dateadd(minute, t.N, cast(s.WorkStart as datetime))
, t.N as MinuteOfWorkDay
, RowNum = row_number() over(order by s.WorkStart, t.N)
FROM #WorkSchedule s
INNER JOIN #Tally t
ON t.N between 1 and datediff(minute, s.WorkStart, s.WorkEnd)
WHERE s.WorkStart >= #workStartDayOne
ORDER BY s.WorkStart, t.N
)
/**/
SELECT #dueDate = m.WorkMinute
FROM cteWorkMins m
WHERE m.RowNum = #SLA_Adjusted
--*/
/**
--DEBUG: this query will show every minute that is accounted for during the Due Date calculation.
SELECT m.*
FROM cteWorkMins m
--WHERE m.RowNum = #SLA_Adjusted
ORDER BY m.WorkMinute
--*/
;
select #dueDate as DueDate;
GO
Test Cleanup:
IF object_id('TEMPDB..#WorkSchedule') IS NOT NULL
DROP TABLE #WorkSchedule;
GO
IF object_id('TEMPDB..#Tally') IS NOT NULL
DROP TABLE #Tally;
GO

as I understood from your question, what you need is as follow
You have given start date and number of minutes added to it, then you need to get the due date
To get the due date, you need to exclude the holidays and the due date should be during business day
here is what you can do
declare #start datetime,
#min int,
#days int
set #start= '28 Dec 2014'
set #min = 2200
-- get the number of days
set #days=datediff(day,#start,dateadd(minute,#min,#start))
-- get the due date
select max(Date)
from
(select row_number() over( order by t.Id)-1 as Id,t.Date
from DateMetadata t
inner join BusinessDays b on Day(t.Date) = b.Day
where t.Date > = #start and not exists(select 1 from Holidays h
where h.Day=Day(t.Date)
and h.Month=Month(t.Date))) as p
where p.Id < #days
Note :that DateMetadata table you will setup it in your database once
the setup for the above code :
create table Holidays(Id int identity(1,1),
Name nvarchar(50),
Day int,
Month int)
create table BusinessDays(Id int identity(1,1),
Name nvarchar(20),
Day int)
-- i am putting some days that are known,
-- it depends on you to define which holidays you want
insert into Holidays (Name,Day,Month) values('Christmas',25,12)
insert into Holidays(Name,Day,Month) values('New Year',31,12)
insert into Holidays(Name,Day,Month) values('Valentine',14,2)
insert into Holidays(Name,Day,Month) values('Mothers day',21,3)
insert into Holidays(Name,Day,Month) values('April fools day',1,4)
-- i am assuming that the business days are from monday till friday and
-- saturday and sunday are off days
insert into BusinessDays(Name,Day) values ('Monday',1)
insert into BusinessDays(Name,Day) values('Tuesday',2)
insert into BusinessDays(Name,Day) values('Wednesday',3)
insert into BusinessDays(Name,Day) values('Thursday',4)
insert into BusinessDays(Name,Day) values('Friday',5)
this table is needed and you will setup it once
-- set up a table that contains all dates from now till 2050 for example
-- and you can change the value of 2050 depending on your needs
-- this table you will setup it once
create table DateMetadata(Id int identity(1,1),
Date datetime)
declare #date datetime
set #date='01 Jan 2014'
while #date < '31 Dec 2050'
begin
insert into DateMetadata(Date) values(#date)
set #date = #date + 1
end
here a working DEMO
if you need any explanation, i am ready
hope it will help you

I created a function to calculate due date from the table, once it's populated as per Beth's and others' approaches (various similar methods for doing this, as you can see -- it only took me about an hour to think about all the UK holidays and populate the table including Easter dates up to 2029 without using these exact guides).
Note that my table contains SLA in business hours (8 hours in a normal day, 5 days in a normal week), your business hours may vary but you can amend this easily, just make sure your business hours are set the same for both the SLA table and the function below.
Code below is T-SQL (written in SSMS v17.8.1)
CREATE FUNCTION [JIRA].[Fn_JIRA_Due_Date] (
#CreatedDate DATETIME, #SLA_Business_Hours INTEGER
) RETURNS DATETIME
AS
-- SELECT [JIRA].[Fn_JIRA_Due_Date]('2019-12-28 08:00:00', 24)
/*
baldmosherâ„¢
2019-03-25
* Function returns the DueDate for a JIRA ticket, based on the CreatedDate and the SLA (based on the Issue Type, or the Epic for Tasks) and business hours per date (set in [JIRA].[Ref_JIRA_Business_Hours])
* Called by IUP to store this at the time the ticket is loaded
* Can only consider SLA in Business Hours:
* <24hrs calendar = <8hrs business
* =24hrs calendar = 8hrs business
* >24hrs calendar = 8hrs business * business days
*/
BEGIN
IF #CreatedDate IS NULL OR #SLA_Business_Hours IS NULL RETURN NULL;
DECLARE #SLA_Hours_Remaining SMALLINT = #SLA_Business_Hours;
--SET DATEFIRST 1;
DECLARE #DueDate DATETIME;
DECLARE #BusHrsStart DECIMAL(18,10) = 8 ; -- start of Business Hours (8am)
DECLARE #BusHrsClose DECIMAL(18,10) = 16 ; -- close of Business Hours (4pm)
--DECLARE #WkndStart DECIMAL(18,10) = 6 ; -- start of weekend (Sat)
DECLARE #Hours_Today SMALLINT ; -- # hours left in day to process ticket
-- PRINT 'Created ' + CAST(CAST(#CreatedDate AS DATE) AS VARCHAR(10)) + ' ' + CAST(CAST(#CreatedDate AS TIME) AS VARCHAR(8))
--!!!! extend to the next whole hour just to simplify reporting -- need to work on fixing this eventually
SET #CreatedDate = DATEADD(MINUTE,60-DATEPART(MINUTE,#CreatedDate),#CreatedDate)
-- PRINT 'Rounded ' + CAST(CAST(#CreatedDate AS DATE) AS VARCHAR(10)) + ' ' + CAST(CAST(#CreatedDate AS TIME) AS VARCHAR(8))
--check if created outside business hours and adjust CreatedDate to start the clock first thing at the next business hours start of day (days are checked next)
IF DATEPART(HOUR,#CreatedDate) < #BusHrsStart
--created before normal hours, adjust #CreatedDate later to #BusHrsStart same day
BEGIN
SET #CreatedDate = DATEADD(HOUR,#BusHrsStart,CAST(CAST(#CreatedDate AS DATE) AS DATETIME))
END
IF DATEPART(HOUR,#CreatedDate) >= #BusHrsClose
--created after normal hours, adjust #CreatedDate to #BusHrsStart next day
BEGIN
SET #CreatedDate = DATEADD(HOUR,#BusHrsStart,CAST(CAST(#CreatedDate+1 AS DATE) AS DATETIME))
--adjust CreatedDate to start the clock the next day with >0 business hours (i.e. extend if it falls on a weekend or holiday)
SET #CreatedDate = CAST(#CreatedDate AS DATE)
StartNextWorkingDay:
IF (SELECT TOP(1) [Business_Hours] FROM [JIRA].[Ref_JIRA_Business_Hours] b WHERE b.[Date] = #CreatedDate ORDER BY [Date]) = 0
BEGIN
SET #CreatedDate = DATEADD(DAY,1,#CreatedDate)
GOTO StartNextWorkingDay
END
--DATEADD(DAY, DATEDIFF(DAY,0,#CreatedDate+7)/7*7,0); -- midnight, Monday next week
SET #CreatedDate = DATEADD(HOUR,#BusHrsStart,#CreatedDate); -- BusHrsStart
END
-- PRINT 'Started ' + CAST(CAST(#CreatedDate AS DATE) AS VARCHAR(10)) + ' ' + CAST(CAST(#CreatedDate AS TIME) AS VARCHAR(8))
--third, check the business hours for each date from CreatedDate onwards to determine the relevant DueDate
SET #DueDate = #CreatedDate
-- PRINT 'SLA Hrs ' + CAST(#SLA_Hours_Remaining AS VARCHAR(2))
SET #Hours_Today = #BusHrsStart + (SELECT TOP(1) [Business_Hours] FROM [JIRA].[Ref_JIRA_Business_Hours] b WHERE b.[Date] = CAST(#DueDate AS DATE) ORDER BY [Date]) - DATEPART(HOUR, #CreatedDate)
-- PRINT 'Hrs Today ' + CAST(#Hours_Today AS VARCHAR(2))
DueNextWorkingDay:
IF #SLA_Hours_Remaining > #Hours_Today
BEGIN
-- PRINT 'Due another day'
SET #SLA_Hours_Remaining = #SLA_Hours_Remaining - #Hours_Today --adjust remaining time after today's hours
SET #Hours_Today = (SELECT TOP(1) [Business_Hours] FROM [JIRA].[Ref_JIRA_Business_Hours] b WHERE b.[Date] = CAST(#DueDate AS DATE) ORDER BY [Date])
-- PRINT 'SLA Hrs ' + CAST(#SLA_Hours_Remaining AS VARCHAR(2))
-- PRINT 'Hrs Today ' + CAST(#Hours_Today AS VARCHAR(2))
SET #DueDate = DATEADD(DAY,1,DATEADD(HOUR,#BusHrsStart,CAST(CAST(#DueDate AS DATE) AS DATETIME))) --adjust DueDate to first thing next day
END
IF #SLA_Hours_Remaining <= #Hours_Today
BEGIN
-- PRINT 'Due today'
SET #DueDate = DATEADD(HOUR,#SLA_Hours_Remaining,#DueDate)
END
ELSE
BEGIN
GOTO DueNextWorkingDay
END
-- PRINT 'DueDate ' + CAST(CAST(#DueDate AS DATE) AS VARCHAR(10)) + ' ' + CAST(CAST(#DueDate AS TIME) AS VARCHAR(8))
RETURN #DueDate
END
GO
Table for SLAs:
CREATE TABLE [JIRA].[Ref_JIRA_SLAs](
[SLA_SK] [SMALLINT] IDENTITY(1,1) NOT NULL,
[Project] [VARCHAR](20) NULL,
[Issue_Type] [VARCHAR](50) NULL,
[Epic_Name] [VARCHAR](50) NULL,
[SLA_Business_Hours] [SMALLINT] NULL,
[Comments] [VARCHAR](8000) NULL
) ON [PRIMARY] WITH (DATA_COMPRESSION = PAGE)
GO

Related

How to add hours to work day in SQL?

I have seen many example adding working date (business days) to date in SQL. But I would like to add hour.
For example; I would like to add 36 hour to date not in Sunday , Saturday
Could one help me about it ?
CREATE FUNCTION AddWorkDays
(
#WorkingDays As Int,
#StartDate AS DateTime
)
RETURNS DateTime
AS
BEGIN
DECLARE #Count AS Int
DECLARE #i As Int
DECLARE #NewDate As DateTime
SET #Count = 0
SET #i = 0
WHILE (#i < #WorkingDays) --runs through the number of days to add
BEGIN
-- increments the count variable
SELECT #Count = #Count + 1
-- increments the i variable
SELECT #i = #i + 1
-- adds the count on to the StartDate and checks if this new date is a Saturday or Sunday
-- if it is a Saturday or Sunday it enters the nested while loop and increments the count variable
WHILE DATEPART(weekday,DATEADD(d, #Count, #StartDate)) IN (1,7)
BEGIN
SELECT #Count = #Count + 1
END
END
-- adds the eventual count on to the Start Date and returns the new date
SELECT #NewDate = DATEADD(d,#Count,#StartDate)
RETURN #NewDate
END
GO
The following will add N hours to a date (excluding Saturday and Sundays).
Example
Declare #Date1 datetime = '2017-04-28'
Declare #Hours int = 36
Select D=max(D)
From (
Select D,HN=-1+Row_Number() over (Order by D)
From (Select D=DateAdd(HOUR,-1+Row_Number() Over (Order By (select null)),#Date1) From master..spt_values n1 ) D
Where DateName(WEEKDAY,D) not in ('Saturday','Sunday')
) D1
Where HN=#Hours
Returns
2017-05-01 12:00:00.000
is it what you are looking for?
declare #num_hours int;
set #num_hours = 1;
select dateadd(HOUR, #num_hours, getdate()) as time_with_hour;
declare #num_hours int;
set #num_hours = 5;
select dateadd(HOUR, #num_hours, getdate()) as time_added,
getdate() as curr_date
This question is already answered Here

How To Get Elapsed Time Between Two Dates, Ignoring Some Time Range?

I have a table with a DATETIME column 'Start' and a DATETIME column 'End'. I want to return the number of minutes between the start and the end (End is always after than Start). Usually I'd just use 'DateDiff()' but this time I need to exclude another date range. For example - From Tuesday at 9am until Wednesday at 6pm, of each week, should be ignored.
If a row has a Start of Tuesday at 8am and an End of Wednesday at 7pm - the elapsed time should be two hours (120 minutes) - because of the ignored date range.
I'm having trouble coming up with a decent way of doing this and my searching online hasn't found quite what I'm looking for. Can someone help me along?
Try This:
--total time span to calculate difference
DECLARE #StartDate DATETIME = '2015-11-10 8:00:00AM',
#EndDate DATETIME = '2015-11-11 7:00:00PM'
--get the day of week (-1 because sunday is counted as first weekday)
DECLARE #StartDayOfWeek INT = (SELECT DATEPART(WEEKDAY, #StartDate)) -1
DECLARE #EndDayOfWeek INT = (SELECT DATEPART(WEEKDAY, #EndDate)) -1
--set the time span to exclude
DECLARE #InitialDOWToExclude TINYINT = 2
DECLARE #InitialTODToExclude VARCHAR(100) = '9:00:00 AM'
DECLARE #EndDOWToExclude TINYINT = 3
DECLARE #EndTODToExclude VARCHAR(100) = '6:00:00 PM'
--this will be the final output in hours
DECLARE #ElapsedHours INT = (SELECT DATEDIFF(HOUR, #StartDate, #EndDate))
DECLARE #WeeksBetween INT = (SELECT DATEDIFF(WEEK, #StartDate, #EndDate))
DECLARE #Iterator INT = 0
WHILE (#Iterator <= #WeeksBetween)
BEGIN
DECLARE #InitialDaysBetween INT = #StartDayOfWeek - #InitialDOWToExclude
DECLARE #StartDateToExclude DATETIME = (SELECT DATEADD(DAY, #InitialDaysBetween, DATEADD(WEEK, #Iterator, #StartDate)))
SET #StartDateToExclude =CAST(DATEPART(YEAR, #StartDateToExclude) AS VARCHAR(100))
+ CAST(DATEPART(MONTH, #StartDateToExclude) AS VARCHAR(100))
+ CAST(DATEPART(DAY, #StartDateToExclude) AS VARCHAR(100))
+ ' '
+ CAST(#InitialTODToExclude AS VARCHAR(100))
DECLARE #EndDaysBetween INT = #EndDayOfWeek - #EndDOWToExclude
DECLARE #EndDateToExclude DATETIME = (SELECT DATEADD(DAY, #EndDaysBetween, DATEADD(WEEK, #Iterator, #EndDate)))
SET #EndDateToExclude =CAST(DATEPART(YEAR, #EndDateToExclude) AS VARCHAR(100))
+ CAST(DATEPART(MONTH, #EndDateToExclude) AS VARCHAR(100))
+ CAST(DATEPART(DAY, #EndDateToExclude) AS VARCHAR(100))
+ ' '
+ CAST(#EndTODToExclude AS VARCHAR(100))
SET #ElapsedHours = #ElapsedHours - DATEDIFF(HOUR, #StartDateToExclude, #EndDateToExclude)
SET #Iterator = #Iterator + 1
END
SELECT #ElapsedHours
This might get you pretty close..
DECLARE #Table1 TABLE ([Id] INT, [Start] DATETIME, [End] DATETIME)
INSERT INTO #Table1 VALUES
(1, '2015-11-08 00:00:00', '2015-11-10 21:45:38'),
(2, '2015-11-09 00:00:00', '2015-11-11 21:45:38')
;
-- hours to exclude
WITH excludeCTE AS
(
SELECT *
FROM (VALUES('Tuesday', 9, 0), ('Wednesday', 0, 0)) AS T([Day], [Hour], [Amount])
UNION ALL
SELECT [Day], [Hour] + 1, [Amount]
FROM excludeCTE
WHERE ([Day] = 'Tuesday' AND [Hour] < 23) OR ([Day] = 'Wednesday' AND [Hour] < 18)
),
-- all hours between start and end
dateCTE AS
(
SELECT [Id],
[Start],
[End],
DATENAME(weekday, [Start])[Day],
DATENAME(hour, [Start])[Hour]
FROM #Table1 t
UNION ALL
SELECT cte.[Id],
DATEADD(HOUR, 1, cte.[Start]),
cte.[End],
DATENAME(weekday, DATEADD(HOUR, 1, cte.[Start]))[Day],
DATENAME(hour, DATEADD(HOUR, 1, cte.[Start]))[Hour]
FROM #Table1 t
JOIN dateCTE cte ON t.Id = cte.Id
WHERE DATEADD(HOUR, 1, cte.[Start]) <= t.[End]
)
SELECT t.[Id],
t.[Start],
t.[End],
SUM(COALESCE(e.[Amount], 1)) [Hours]
FROM #Table1 t
INNER JOIN dateCTE d ON t.[Id] = d.[Id]
LEFT JOIN excludeCTE e ON d.[Day] = e.[Day] AND d.[Hour] = e.[Hour]
GROUP BY t.[Id],
t.[Start],
t.[End]
OPTION (MAXRECURSION 0) -- allow more than 100 hours
Putting the additional constraint that there can only be one excluded range between any two date
CREATE TABLE worktable (
_Id INT
, _Start DATETIME
, _End DATETIME
);
INSERT INTO worktable VALUES
(1, '2015-11-09 00:00:00', '2015-11-09 00:45:00') -- Start and End before excluded range
, (2, '2015-11-09 00:00:00', '2015-11-11 21:45:00') -- Start before, End after
, (3, '2015-11-09 00:00:00', '2015-11-10 21:00:00') -- Start before, End between
, (4, '2015-11-10 10:00:00', '2015-11-11 10:00:00') -- Start between, End between
, (5, '2015-11-10 10:00:00', '2015-11-11 21:45:00') -- Start between, End after
With getDates As (
SELECT _Id
, a = _Start
, b = _End
, c = DATEADD(hh, 9
, DATEADD(DAY,DATEDIFF(DAY, 0, _Start) / 7 * 7
+ 7 * Cast(Sign(1 - DatePart(dw, _Start)) + 1 as bit), 1))
, d = DATEADD(hh, 18
, DATEADD(DAY,DATEDIFF(DAY, 0, _Start) / 7 * 7
+ 7 * Cast(Sign(1 - DatePart(dw, _Start)) + 1 as bit), 2))
FROM worktable
), getDiff As (
SELECT c_a = DATEDIFF(mi, a, c)
, c_b = DATEDIFF(mi, b, c)
, b_d = DATEDIFF(mi, d, b)
, a, b, c, d, _id
FROM getDates
)
Select _id
, (c_a + ABS(c_a)) / 2
- (c_b + ABS(c_b)) / 2
+ (b_d + ABS(b_d)) / 2
FROM getDiff;
c is the date of the first Tuesday after the start date (Find the next occurance of a day of the week in SQL) you may need to adjust the last value depending from DATEFIRST
d is the date of the first Wednesday after the start date in the same week of c
Cast(Sign(a - b) + 1 as bit) is 1 if a is more than or equal b, 0 otherwise
(x + ABS(x)) / 2 is x if not negative, otherwise 0
Given that the formula to get the elapsed time with the excluded range is:
+ (Exclusion Start - Start) If (Start < Exclusion Start)
- (Exclusion Start - End) If (End < Exclusion Start)
+ (End - Exclusion End) If (Exclusion End < End)
-- excluded range (weekday numbers run from 1 to 7)
declare #x datetime = /*ignore*/ '1900012' + /*start day # and time*/ '3 09:00am';
declare #y datetime = /*ignore*/ '1900012' + /* end day # and time*/ '4 06:00pm';
-- normalize date to 1900-01-21, which was a Sunday
declare #s datetime =
dateadd(day, 19 + datepart(weekday, #start), cast(cast(#start as time) as datetime));
declare #e datetime =
dateadd(day, 19 + datepart(weekday, #end), cast(cast(#end as time) as datetime));
-- split range into two parts, one before #x and the other after #y
-- each part collapses to zero when #s and #e respectively fall between #x and #y
select (
datediff(second, -- diff in minutes would truncate so count seconds
case when #s < #x then #s else #x end, -- minimum of #s, #x
case when #e < #x then #e else #x end -- minimum of #e, #x
) +
datediff(second,
case when #s > #y then #s else #y end, -- maximum of #s, #y
case when #e > #y then #e else #y end -- maximum of #e, #y
)
) / 60; -- convert seconds to minutes, truncating with integer division
I glanced at the earlier answers and I thought that surely there was something more straightforward and elegant. Perhaps this is easier to understand and one clear advantage over some solutions is that it's trivial to change the excluded range and that range doesn't have to be limited to a single day.
I'm assuming that your dates never span more than one regular calendar week. It wouldn't be too difficult to extend it to handle more though. One approach would be to handle starting and ending partial weeks plus the full weeks in the middle.
Imagine that your start time is 8:59:30am and your end time is 6:00:30pm. In such a case I'm figuring that you'd want to accumulate the half minutes on each side to get a full minute in total after subtracting the 9-6 block. If you use datediff(minute, ...) you would be truncating the partial minutes and never get the chance to add them together: so that's why I count seconds and then divide by sixty at the end. Of course, if you're only dealing in whole minutes then you won't need to do it that way.
I've chosen my reference date somewhat arbitrarily. At first I thought it might possibly be handy to look at a real and convenient date on the calendar but ultimately it only really matters that it falls on a Sunday. So I settled on the first Sunday falling on a date ending in the digit 1.
Note that the solution also relies on datefirst being set to Sunday. That could be tweaked or made more portable if necessary.

Datepart query shows value 1 for saturday

The following is my stored procedure for setting workingday between two dates
Create procedure [dbo].[sp_workingdays](#startdate date,#enddate date,#createddatetime datetime,#adminid int)
as
declare #start date,#end date
declare #day varchar(50)
declare #timeid int
declare #daypare int
declare #workingdaytype varchar(20)
begin
set #start=#startdate
set #end=#enddate
while (#start<=#end)
begin
if #start not in (select Date from Generalholyday_details)
begin
select #day= DATENAME(dw,#start)
select #workingdaytype =case (DATEPART(DW,#start)+##DATEFIRST)%7 when 1 then 'Leave Day' when 2 then 'Full Day' when 3 then 'Full Day' when 4 then 'Full Day'when 5 then 'Full Day' when 6 then 'Full Day' when 0 then 'Half Day' end
select #timeid=Time_id from Workingdaytimesetting_details where Workingday_type=#workingdaytype
insert into Workingday_details(Working_date,working_day,Time_id) values(#start,#day,#timeid)
update Workingday_details set createddatetime=#createddatetime where createddatetime is null
update Workingday_details set adminid=#adminid where adminid is null
end
set #start=DATEADD(day,1,#start)
end
end
GO
In datepart line 1 is for sunday. but actually when i run the stored procedure, i got 1 for Saturday. how can i set 1 for sunday.In my previous system i got 1 for sunday in same procedure.
Have a look at SET DATEFIRST (Transact-SQL)
Sets the first day of the week to a number from 1 through 7.
To see the current setting of SET DATEFIRST, use the ##DATEFIRST
function.
The setting of SET DATEFIRST is set at execute or run time and not at
parse time.
Specifying SET DATEFIRST has no effect on DATEDIFF. DATEDIFF always
uses Sunday as the first day of the week to ensure the function is
deterministic.
Try this one -
SET DATEFIRST 7
DATEFIRST - MSDN
Also try this query after small refactor:
CREATE PROCEDURE [dbo].[sp_workingdays]
(
#startdate DATE
, #enddate DATE
, #createddatetime DATETIME
, #adminid INT
)
AS BEGIN
DECLARE
#start DATE
, #end DATE
, #day VARCHAR(50)
, #timeid INT
, #workingdaytype VARCHAR(20)
SELECT
#start = #startdate
, #end = #enddate
WHILE (#start <= #end) BEGIN
IF NOT EXISTS(
SELECT 1
FROM Generalholyday_details
WHERE #start = [Date]
) BEGIN
SELECT
#day = DATENAME(dw, #start)
, #workingdaytype =
CASE WHEN dt = 1 THEN 'Leave Day'
WHEN dt IN (2,3,4,5,6) THEN 'Full Day'
ELSE 'Half Day'
END
FROM (
SELECT dt = (DATEPART(DW, #start) + ##DATEFIRST) % 7
) t
SELECT #timeid = Time_id
FROM Workingdaytimesetting_details
WHERE Workingday_type = #workingdaytype
INSERT INTO Workingday_details (Working_date, working_day, Time_id)
VALUES (#start, #day, #timeid)
UPDATE Workingday_details
SET createddatetime = #createddatetime
WHERE createddatetime IS NULL
UPDATE Workingday_details
SET adminid = #adminid
WHERE adminid IS NULL
END
SET #start = DATEADD(DAY, 1, #start)
END
END
GO

T-SQL get number of working days between 2 dates

I want to calculate the number of working days between 2 given dates. For example if I want to calculate the working days between 2013-01-10 and 2013-01-15, the result must be 3 working days (I don't take into consideration the last day in that interval and I subtract the Saturdays and Sundays). I have the following code that works for most of the cases, except the one in my example.
SELECT (DATEDIFF(day, '2013-01-10', '2013-01-15'))
- (CASE WHEN DATENAME(weekday, '2013-01-10') = 'Sunday' THEN 1 ELSE 0 END)
- (CASE WHEN DATENAME(weekday, DATEADD(day, -1, '2013-01-15')) = 'Saturday' THEN 1 ELSE 0 END)
How can I accomplish this? Do I have to go through all the days and check them? Or is there an easy way to do this.
Please, please, please use a calendar table. SQL Server doesn't know anything about national holidays, company events, natural disasters, etc. A calendar table is fairly easy to build, takes an extremely small amount of space, and will be in memory if it is referenced enough.
Here is an example that creates a calendar table with 30 years of dates (2000 -> 2029) but requires only 200 KB on disk (136 KB if you use page compression). That is almost guaranteed to be less than the memory grant required to process some CTE or other set at runtime.
CREATE TABLE dbo.Calendar
(
dt DATE PRIMARY KEY, -- use SMALLDATETIME if < SQL Server 2008
IsWorkDay BIT
);
DECLARE #s DATE, #e DATE;
SELECT #s = '2000-01-01' , #e = '2029-12-31';
INSERT dbo.Calendar(dt, IsWorkDay)
SELECT DATEADD(DAY, n-1, '2000-01-01'), 1
FROM
(
SELECT TOP (DATEDIFF(DAY, #s, #e)+1) ROW_NUMBER()
OVER (ORDER BY s1.[object_id])
FROM sys.all_objects AS s1
CROSS JOIN sys.all_objects AS s2
) AS x(n);
SET DATEFIRST 1;
-- weekends
UPDATE dbo.Calendar SET IsWorkDay = 0
WHERE DATEPART(WEEKDAY, dt) IN (6,7);
-- Christmas
UPDATE dbo.Calendar SET IsWorkDay = 0
WHERE MONTH(dt) = 12
AND DAY(dt) = 25
AND IsWorkDay = 1;
-- continue with other holidays, known company events, etc.
Now the query you're after is quite simple to write:
SELECT COUNT(*) FROM dbo.Calendar
WHERE dt >= '20130110'
AND dt < '20130115'
AND IsWorkDay = 1;
More info on calendar tables:
http://web.archive.org/web/20070611150639/http://sqlserver2000.databases.aspfaq.com/why-should-i-consider-using-an-auxiliary-calendar-table.html
More info on generating sets without loops:
http://www.sqlperformance.com/tag/date-ranges
Also beware of little things like relying on the English output of DATENAME. I've seen several applications break because some users had a different language setting, and if you're relying on WEEKDAY be sure you set your DATEFIRST setting appropriately...
For stuff like this i tend to maintain a calendar table that also includes bank holidays etc.
The script i use for this is as follows (Note that i didnt write it # i forget where i found it)
SET DATEFIRST 1
SET NOCOUNT ON
GO
--Create ISO week Function (thanks BOL)
CREATE FUNCTION ISOweek ( #DATE DATETIME )
RETURNS INT
AS
BEGIN
DECLARE #ISOweek INT
SET #ISOweek = DATEPART(wk, #DATE) + 1 - DATEPART(wk, CAST(DATEPART(yy, #DATE) AS CHAR(4)) + '0104')
--Special cases: Jan 1-3 may belong to the previous year
IF ( #ISOweek = 0 )
SET #ISOweek = dbo.ISOweek(CAST(DATEPART(yy, #DATE) - 1 AS CHAR(4)) + '12' + CAST(24 + DATEPART(DAY, #DATE) AS CHAR(2))) + 1
--Special case: Dec 29-31 may belong to the next year
IF ( ( DATEPART(mm, #DATE) = 12 )
AND ( ( DATEPART(dd, #DATE) - DATEPART(dw, #DATE) ) >= 28 )
)
SET #ISOweek = 1
RETURN(#ISOweek)
END
GO
--END ISOweek
--CREATE Easter algorithm function
--Thanks to Rockmoose (http://www.sqlteam.com/forums/topic.asp?TOPIC_ID=45689)
CREATE FUNCTION fnDLA_GetEasterdate ( #year INT )
RETURNS CHAR(8)
AS
BEGIN
-- Easter date algorithm of Delambre
DECLARE #A INT ,
#B INT ,
#C INT ,
#D INT ,
#E INT ,
#F INT ,
#G INT ,
#H INT ,
#I INT ,
#K INT ,
#L INT ,
#M INT ,
#O INT ,
#R INT
SET #A = #YEAR % 19
SET #B = #YEAR / 100
SET #C = #YEAR % 100
SET #D = #B / 4
SET #E = #B % 4
SET #F = ( #B + 8 ) / 25
SET #G = ( #B - #F + 1 ) / 3
SET #H = ( 19 * #A + #B - #D - #G + 15 ) % 30
SET #I = #C / 4
SET #K = #C % 4
SET #L = ( 32 + 2 * #E + 2 * #I - #H - #K ) % 7
SET #M = ( #A + 11 * #H + 22 * #L ) / 451
SET #O = 22 + #H + #L - 7 * #M
IF #O > 31
BEGIN
SET #R = #O - 31 + 400 + #YEAR * 10000
END
ELSE
BEGIN
SET #R = #O + 300 + #YEAR * 10000
END
RETURN #R
END
GO
--END fnDLA_GetEasterdate
--Create the table
CREATE TABLE MyDateTable
(
FullDate DATETIME NOT NULL
CONSTRAINT PK_FullDate PRIMARY KEY CLUSTERED ,
Period INT ,
ISOWeek INT ,
WorkingDay VARCHAR(1) CONSTRAINT DF_MyDateTable_WorkDay DEFAULT 'Y'
)
GO
--End table create
--Populate table with required dates
DECLARE #DateFrom DATETIME ,
#DateTo DATETIME ,
#Period INT
SET #DateFrom = CONVERT(DATETIME, '20000101')
--yyyymmdd (1st Jan 2000) amend as required
SET #DateTo = CONVERT(DATETIME, '20991231')
--yyyymmdd (31st Dec 2099) amend as required
WHILE #DateFrom <= #DateTo
BEGIN
SET #Period = CONVERT(INT, LEFT(CONVERT(VARCHAR(10), #DateFrom, 112), 6))
INSERT MyDateTable
( FullDate ,
Period ,
ISOWeek
)
SELECT #DateFrom ,
#Period ,
dbo.ISOweek(#DateFrom)
SET #DateFrom = DATEADD(dd, +1, #DateFrom)
END
GO
--End population
/* Start of WorkingDays UPDATE */
UPDATE MyDateTable
SET WorkingDay = 'B' --B = Bank Holiday
--------------------------------EASTER---------------------------------------------
WHERE FullDate = DATEADD(dd, -2, CONVERT(DATETIME, dbo.fnDLA_GetEasterdate(DATEPART(yy, FullDate)))) --Good Friday
OR FullDate = DATEADD(dd, +1, CONVERT(DATETIME, dbo.fnDLA_GetEasterdate(DATEPART(yy, FullDate))))
--Easter Monday
GO
UPDATE MyDateTable
SET WorkingDay = 'B'
--------------------------------NEW YEAR-------------------------------------------
WHERE FullDate IN ( SELECT MIN(FullDate)
FROM MyDateTable
WHERE DATEPART(mm, FullDate) = 1
AND DATEPART(dw, FullDate) NOT IN ( 6, 7 )
GROUP BY DATEPART(yy, FullDate) )
---------------------MAY BANK HOLIDAYS(Always Monday)------------------------------
OR FullDate IN ( SELECT MIN(FullDate)
FROM MyDateTable
WHERE DATEPART(mm, FullDate) = 5
AND DATEPART(dw, FullDate) = 1
GROUP BY DATEPART(yy, FullDate) )
OR FullDate IN ( SELECT MAX(FullDate)
FROM MyDateTable
WHERE DATEPART(mm, FullDate) = 5
AND DATEPART(dw, FullDate) = 1
GROUP BY DATEPART(yy, FullDate) )
--------------------AUGUST BANK HOLIDAY(Always Monday)------------------------------
OR FullDate IN ( SELECT MAX(FullDate)
FROM MyDateTable
WHERE DATEPART(mm, FullDate) = 8
AND DATEPART(dw, FullDate) = 1
GROUP BY DATEPART(yy, FullDate) )
--------------------XMAS(Move to next working day if on Sat/Sun)--------------------
OR FullDate IN ( SELECT CASE WHEN DATEPART(dw, FullDate) IN ( 6, 7 ) THEN DATEADD(dd, +2, FullDate)
ELSE FullDate
END
FROM MyDateTable
WHERE DATEPART(mm, FullDate) = 12
AND DATEPART(dd, FullDate) IN ( 25, 26 ) )
GO
---------------------------------------WEEKENDS--------------------------------------
UPDATE MyDateTable
SET WorkingDay = 'N'
WHERE DATEPART(dw, FullDate) IN ( 6, 7 )
GO
/* End of WorkingDays UPDATE */
--SELECT * FROM MyDateTable ORDER BY 1
DROP FUNCTION fnDLA_GetEasterdate
DROP FUNCTION ISOweek
--DROP TABLE MyDateTable
SET NOCOUNT OFF
Once you have created the table, finding the number of working days is easy peasy:
SELECT COUNT(FullDate) AS WorkingDays
FROM dbo.tbl_WorkingDays
WHERE WorkingDay = 'Y'
AND FullDate >= CONVERT(DATETIME, '10/01/2013', 103)
AND FullDate < CONVERT(DATETIME, '15/01/2013', 103)
Note that this script includes UK bank holidays, i'm not sure what region you're in.
Here's a simple function that counts working days not including Saturday and Sunday (when counting holidays isn't necessary):
CREATE FUNCTION dbo.udf_GetBusinessDays (
#START_DATE DATE,
#END_DATE DATE
)
RETURNS INT
WITH EXECUTE AS CALLER
AS
BEGIN
DECLARE #NUMBER_OF_DAYS INT = 0;
DECLARE #DAY_COUNTER INT = 0;
DECLARE #BUSINESS_DAYS INT = 0;
DECLARE #CURRENT_DATE DATE;
DECLARE #DAYNAME NVARCHAR(9)
SET #NUMBER_OF_DAYS = DATEDIFF(DAY, #START_DATE, #END_DATE);
WHILE #DAY_COUNTER <= #NUMBER_OF_DAYS
BEGIN
SET #CURRENT_DATE = DATEADD(DAY, #DAY_COUNTER, #START_DATE)
SET #DAYNAME = DATENAME(WEEKDAY, #CURRENT_DATE)
SET #DAY_COUNTER += 1
IF #DAYNAME = N'Saturday' OR #DAYNAME = N'Sunday'
BEGIN
CONTINUE
END
ELSE
BEGIN
SET #BUSINESS_DAYS += 1
END
END
RETURN #BUSINESS_DAYS
END
GO
This is the method I normally use (When not using a calendar table):
DECLARE #T TABLE (Date1 DATE, Date2 DATE);
INSERT #T VALUES ('20130110', '20130115'), ('20120101', '20130101'), ('20120611', '20120701');
SELECT Date1, Date2, WorkingDays
FROM #T t
CROSS APPLY
( SELECT [WorkingDays] = COUNT(*)
FROM Master..spt_values s
WHERE s.Number BETWEEN 1 AND DATEDIFF(DAY, t.date1, t.Date2)
AND s.[Type] = 'P'
AND DATENAME(WEEKDAY, DATEADD(DAY, s.number, t.Date1)) NOT IN ('Saturday', 'Sunday')
) wd
If like I do you have a table with holidays in you can add this in too:
SELECT Date1, Date2, WorkingDays
FROM #T t
CROSS APPLY
( SELECT [WorkingDays] = COUNT(*)
FROM Master..spt_values s
WHERE s.Number BETWEEN 1 AND DATEDIFF(DAY, t.date1, t.Date2)
AND s.[Type] = 'P'
AND DATENAME(WEEKDAY, DATEADD(DAY, s.number, t.Date1)) NOT IN ('Saturday', 'Sunday')
AND NOT EXISTS
( SELECT 1
FROM HolidayTable ht
WHERE ht.Date = DATEADD(DAY, s.number, t.Date1)
)
) wd
The above will only work if your dates are within 2047 days of each other, if you are likely to be calculating larger date ranges you can use this:
SELECT Date1, Date2, WorkingDays
FROM #T t
CROSS APPLY
( SELECT [WorkingDays] = COUNT(*)
FROM ( SELECT [Number] = ROW_NUMBER() OVER(ORDER BY s.number)
FROM Master..spt_values s
CROSS JOIN Master..spt_values s2
) s
WHERE s.Number BETWEEN 1 AND DATEDIFF(DAY, t.date1, t.Date2)
AND DATENAME(WEEKDAY, DATEADD(DAY, s.number, t.Date1)) NOT IN ('Saturday', 'Sunday')
) wd
I did my code in SQL SERVER 2008 (MS SQL) . It works fine for me. I hope it will help you.
DECLARE #COUNTS int,
#STARTDATE date,
#ENDDATE date
SET #STARTDATE ='01/21/2013' /*Start date in mm/dd/yyy */
SET #ENDDATE ='01/26/2013' /*End date in mm/dd/yyy */
SET #COUNTS=0
WHILE (#STARTDATE<=#ENDDATE)
BEGIN
/*Check for holidays*/
IF ( DATENAME(weekday,#STARTDATE)<>'Saturday' and DATENAME(weekday,#STARTDATE)<>'Sunday')
BEGIN
SET #COUNTS=#COUNTS+1
END
SET #STARTDATE=DATEADD(day,1,#STARTDATE)
END
/* Display the no of working days */
SELECT #COUNTS
By Combining #Aaron Bertrand's answer and the Easter Calculation from #HeavenCore's and adding some code of my own, this code creates a calendar from 2000 to 2049 that includes UK (England) Bank Holidays. Usage and notes as per Aaron's answer:
DECLARE #s DATE, #e DATE;
SELECT #s = '2000-01-01' , #e = '2049-12-31';
-- Insert statements for procedure here
CREATE TABLE dbo.Calendar
(
dt DATE PRIMARY KEY, -- use SMALLDATETIME if < SQL Server 2008
IsWorkDay BIT
);
INSERT dbo.Calendar(dt, IsWorkDay)
SELECT DATEADD(DAY, n-1, '2000-01-01'), 1
FROM
(
SELECT TOP (DATEDIFF(DAY, #s, #e)+1) ROW_NUMBER()
OVER (ORDER BY s1.[object_id])
FROM sys.all_objects AS s1
CROSS JOIN sys.all_objects AS s2
) AS x(n);
SET DATEFIRST 1;
-- weekends
UPDATE dbo.Calendar SET IsWorkDay = 0
WHERE DATEPART(WEEKDAY, dt) IN (6,7);
-- Christmas
UPDATE dbo.Calendar SET IsWorkDay = 0
WHERE IsWorkDay = 1 and MONTH(dt) = 12 and
(DAY(dt) in (25,26) or
(DAY(dt) in (27, 28) and DATEPART(WEEKDAY, dt) IN (1,2)) );
-- New Year
UPDATE dbo.Calendar SET IsWorkDay = 0
WHERE IsWorkDay = 1 and MONTH(dt) = 1 AND
( DAY(dt) = 1 or (DAY(dt) IN (2,3) AND DATEPART(WEEKDAY, dt)=1 ));
-- Easter
UPDATE dbo.Calendar SET IsWorkDay = 0
WHERE dt = DATEADD(dd, -2, CONVERT(DATETIME, dbo.fnDLA_GetEasterdate(DATEPART(yy, dt)))) --Good Friday
OR dt = DATEADD(dd, +1, CONVERT(DATETIME, dbo.fnDLA_GetEasterdate(DATEPART(yy, dt)))) --Easter Monday
-- May Day (first Monday in May)
UPDATE dbo.Calendar SET IsWorkDay = 0
WHERE MONTH(dt) = 5 AND DATEPART(WEEKDAY, dt)=1 and DAY(DT)<8;
-- Spring Bank Holiday (last Monday in May apart from 2022 when moved to include Platinum Jubilee bank holiday)
UPDATE dbo.Calendar SET IsWorkDay = 0
WHERE
(YEAR(dt)=2022 and MONTH(dt) = 6 AND DAY(dt) IN (2,3)) OR
(YEAR(dt)<>2022 and MONTH(dt) = 5 AND DATEPART(WEEKDAY, dt)=1 and DAY(DT)>24);
-- Summer Bank Holiday (last Monday in August)
UPDATE dbo.Calendar SET IsWorkDay = 0
WHERE MONTH(dt) = 8 AND DATEPART(WEEKDAY, dt)=1 and DAY(DT)>24;

SQL Nth Day of Nth Week of a Month

I want to use the following function for scheduling club meetings which occur on a monthly basis based on the week and weekday of the month. In the example below I have a (to be) function that returns the Third Wednesday of the month. If that day occurs in the past then it returns the next month's 3rd Wednesday.
I want to get away from the Loops and I feel that there is a better method for calculation. Is there a more OO process? Your opinion?
--CREATE FUNCTION NextWeekDayofMonth
DECLARE
--(
#WEEK INT,
#WEEKDAY INT,
#REFERENCEDATE DATETIME
--)
--RETURNS DATETIME
--AS
-------------------------------
--Values for testing - Third Wednesday of the Month
set #WEEK = 3 --Third Week
set #WEEKDAY = 4 --Wednesday
set #REFERENCEDATE = '08/20/2011'
-------------------------------
BEGIN
DECLARE #WEEKSEARCH INT
DECLARE #FDOM DATETIME
DECLARE #RETURNDATE DATETIME
SET #FDOM = DATEADD(M,DATEDIFF(M,0,#REFERENCEDATE),0)
SET #RETURNDATE = DATEADD(M,0,#FDOM)
WHILE (#RETURNDATE < #REFERENCEDATE)
--If the calculated date occurs in the past then it
--finds the appropriate date in the next month
BEGIN
SET #WEEKSEARCH = 1
SET #RETURNDATE = #FDOM
--Finds the first weekday of the month that matches the provided weekday value
WHILE ( DATEPART(DW,#RETURNDATE) <> #WEEKDAY)
BEGIN
SET #RETURNDATE = DATEADD(D,1,#RETURNDATE)
END
--Iterates through the weeks without going into next month
WHILE #WEEKSEARCH < #WEEK
BEGIN
IF MONTH(DATEADD(WK,1,#RETURNDATE)) = MONTH(#FDOM)
BEGIN
SET #RETURNDATE = DATEADD(WK,1,#RETURNDATE)
SET #WEEKSEARCH = #WEEKSEARCH+1
END
ELSE
BREAK
END
SET #FDOM = DATEADD(M,1,#FDOM)
END
--RETURN #RETURNDATE
select #ReturnDate
END
IMO, the best process is to store important business information as rows in tables in a database. If you build a calendar table, you can get all the third Wednesdays by a simple query. Not only are the queries simple, they can be seen to be obviously correct.
select cal_date
from calendar
where day_of_week_ordinal = 3
and day_of_week = 'Wed';
The third Wednesday that's on or after today is also simple.
select min(cal_date)
from calendar
where day_of_week_ordinal = 3
and day_of_week = 'Wed'
and cal_date >= CURRENT_DATE;
Creating a calendar table is straightforward. This was written for PostgreSQL, but it's entirely standard SQL (I think) except for the columns relating to ISO years and ISO weeks.
create table calendar (
cal_date date primary key,
year_of_date integer not null
check (year_of_date = extract(year from cal_date)),
month_of_year integer not null
check (month_of_year = extract(month from cal_date)),
day_of_month integer not null
check (day_of_month = extract(day from cal_date)),
day_of_week char(3) not null
check (day_of_week =
case when extract(dow from cal_date) = 0 then 'Sun'
when extract(dow from cal_date) = 1 then 'Mon'
when extract(dow from cal_date) = 2 then 'Tue'
when extract(dow from cal_date) = 3 then 'Wed'
when extract(dow from cal_date) = 4 then 'Thu'
when extract(dow from cal_date) = 5 then 'Fri'
when extract(dow from cal_date) = 6 then 'Sat'
end),
day_of_week_ordinal integer not null
check (day_of_week_ordinal =
case
when day_of_month >= 1 and day_of_month <= 7 then 1
when day_of_month >= 8 and day_of_month <= 14 then 2
when day_of_month >= 15 and day_of_month <= 21 then 3
when day_of_month >= 22 and day_of_month <= 28 then 4
else 5
end),
iso_year integer not null
check (iso_year = extract(isoyear from cal_date)),
iso_week integer not null
check (iso_week = extract(week from cal_date))
);
You can populate that table with a spreadsheet or with a UDF. Spreadsheets usually have pretty good date and time functions. I have a UDF, but it's written for PostgreSQL (PL/PGSQL), so I'm not sure how much it would help you. But I'll post it later if you like.
Here's a date math way to accomplish what you want without looping:
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Description: Gets the nth occurrence of a given weekday in the month containing the specified date.
-- For #dayOfWeek, 1 = Sunday, 2 = Monday, 3 = Tuesday, 4 = Wednesday, 5 = Thursday, 6 = Friday, 7 = Saturday
-- =============================================
CREATE FUNCTION GetWeekdayInMonth
(
#date datetime,
#dayOfWeek int,
#nthWeekdayInMonth int
)
RETURNS datetime
AS
BEGIN
DECLARE #beginMonth datetime
DECLARE #offSet int
DECLARE #firstWeekdayOfMonth datetime
DECLARE #result datetime
SET #beginMonth = DATEADD(DAY, -DATEPART(DAY, #date) + 1, #date)
SET #offSet = #dayOfWeek - DATEPART(dw, #beginMonth)
IF (#offSet < 0)
BEGIN
SET #firstWeekdayOfMonth = DATEADD(d, 7 + #offSet, #beginMonth)
END
ELSE
BEGIN
SET #firstWeekdayOfMonth = DATEADD(d, #offSet, #beginMonth)
END
SET #result = DATEADD(WEEK, #nthWeekdayInMonth - 1, #firstWeekdayOfMonth)
IF (NOT(MONTH(#beginMonth) = MONTH(#result)))
BEGIN
SET #result = NULL
END
RETURN #result
END
GO
DECLARE #nextMeetingDate datetime
SET #nextMeetingDate = dbo.GetWeekdayInMonth(GETDATE(), 4, 3)
IF (#nextMeetingDate IS NULL OR #nextMeetingDate < GETDATE())
BEGIN
SET #nextMeetingDate = dbo.GetWeekDayInMonth(DATEADD(MONTH, 1, GETDATE()), 4, 3)
END
SELECT #nextMeetingDate
Here's another function based solution using date math that returns the Next Nth Weekday on or after a given date. It doesn't use any looping to speak of, but it may recurs by at most one iteration if the next Nth weekday is in the next month.
This function takes the DATEFIRST setting into account for environments that use a value other than the default.
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Author: David Grimberg
-- Create date: 2015-06-18
-- Description: Gets the next Nth weekday
-- #param Date is any date in a month
-- #param DayOfWeek is the weekday of interest ranging
-- from 1 to 7 with ##DATEFIRST being the
-- first day of the week
-- #param NthWeekday represents which ordinal weekday to return.
-- Positive values return dates relative to the start
-- of the month. Negative values return dates relative
-- to the end of the month. Values > 4 indicate the
-- last week, values < -4 indicate the first week.
-- Zero is assumed to be 1.
-- =============================================
ALTER FUNCTION dbo.xxGetNextNthWeekday
(
#Date date,
#NthWeekday smallint,
#DayOfWeek tinyint
)
RETURNS date
AS
BEGIN
DECLARE #FirstOfMonth date
DECLARE #inc int
DECLARE #Result date
-- Clamp the #NthWeekday input to valid values
set #NthWeekday = case when #NthWeekday = 0 then 1
when #NthWeekday > 4 then -1
when #NthWeekday < -4 then 1
else #NthWeekday
end
-- Normalize the requested day of week taking
-- ##DATEFIRST into consideration.
set #DayOfWeek = (##DATEFIRST + 6 + #DayOfWeek) % 7 + 1
-- Gets the first of the current month or the
-- next month if #NthWeekday is negative.
set #FirstOfMonth = dateadd(month, datediff(month,0,#Date)
+ case when #NthWeekday < 0 then 1 else 0 end
, 0)
-- Add and/or subtract 1 week depending direction of search and the
-- relationship of #FirstOfMonth's Day of the Week to the #DayOfWeek
-- of interest
set #inc = case when (datepart(WEEKDAY, #FirstOfMonth)+##DATEFIRST-1)%7+1 > #DayOfWeek
then 0
else -1
end
+ case when #NthWeekday < 0 then 1 else 0 end
-- Put it all together
set #Result = dateadd( day
, #DayOfWeek-1
, dateadd( WEEK
, #NthWeekday + #inc
, dateadd( WEEK -- Gets 1st Sunday on or
, datediff(WEEK, -1, #FirstOfMonth)
,-1 ) ) ) -- before #FirstOfMonth
-- [Snip here] --
if #Result < #Date
set #Result = dbo.xxGetNextNthWeekday( dateadd(month, datediff(month, 0, #Date)+1, 0)
, #NthWeekday, #DayOfWeek)
-- [to here for no recursion] --
Return #Result
END
If you want the past or future Nth weekday of the current month based on the #Date parameter rather than the next Nth weekday snip out the recursive part as indicated above.