Calculate seconds between two dates including business hours, excluding holidays, start hour 08:30 - sql

When looking for a solution for my question I found the following sql (Calculate business hours between two dates).
However, I want the starting point to be 8:30 instead of 9 and the end point 17:30 instead of 17. Currently I am using an integer. Can anyone help me with this? Thanks in advance!
Create Function GetWorkingMin(#StartDate DateTime, #EndDate DateTime, #Country Varchar(2)) Returns Int
AS
Begin
Declare #WorkMin int = 0 -- Initialize counter
Declare #Reverse bit -- Flag to hold if direction is reverse
Declare #StartHour int = 9 -- Start of business hours (can be supplied as an argument if needed)
Declare #EndHour int = 17 -- End of business hours (can be supplied as an argument if needed)
Declare #Holidays Table (HDate DateTime) -- Table variable to hold holidayes
-- If dates are in reverse order, switch them and set flag
If #StartDate>#EndDate
Begin
Declare #TempDate DateTime=#StartDate
Set #StartDate=#EndDate
Set #EndDate=#TempDate
Set #Reverse=1
End
Else Set #Reverse = 0
-- 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 HDate from HOLIDAY Where COUNTRYCODE=#Country and HDATE>=DateAdd(dd, DateDiff(dd,0,#StartDate), 0)
If DatePart(HH, #StartDate)<#StartHour Set #StartDate = DateAdd(hour, #StartHour, DateDiff(DAY, 0, #StartDate)) -- If Start time is less than start hour, set it to start hour
If DatePart(HH, #StartDate)>=#EndHour+1 Set #StartDate = DateAdd(hour, #StartHour+24, DateDiff(DAY, 0, #StartDate)) -- If Start time is after end hour, set it to start hour of next day
If DatePart(HH, #EndDate)>=#EndHour+1 Set #EndDate = DateAdd(hour, #EndHour, DateDiff(DAY, 0, #EndDate)) -- If End time is after end hour, set it to end hour
If DatePart(HH, #EndDate)<#StartHour Set #EndDate = DateAdd(hour, #EndHour-24, DateDiff(DAY, 0, #EndDate)) -- If End time is before start hour, set it to end hour of previous day
If #StartDate>#EndDate Return 0
-- If Start and End is on same day
If DateDiff(Day,#StartDate,#EndDate) <= 0
Begin
If Datepart(dw,#StartDate)>1 And DATEPART(dw,#StartDate)<7 -- If day is between sunday and saturday
If (Select Count(*) From #Holidays Where HDATE=DateAdd(dd, DateDiff(dd,0,#StartDate), 0)) = 0 -- If day is not a holiday
If #EndDate<#StartDate Return 0 Else Set #WorkMin=DATEDIFF(MI, #StartDate, #EndDate) -- Calculate difference
Else Return 0
Else Return 0
End
Else Begin
Declare #Partial int=1 -- Set partial day flag
While DateDiff(Day,#StartDate,#EndDate) > 0 -- While start and end days are different
Begin
If Datepart(dw,#StartDate)>1 And DATEPART(dw,#StartDate)<7 -- If this is a weekday
Begin
If (Select Count(*) From #Holidays Where HDATE=DateAdd(dd, DateDiff(dd,0,#StartDate), 0)) = 0 -- If this is not a holiday
Begin
If #Partial=1 -- If this is the first iteration, calculate partial time
Begin
Set #WorkMin=#WorkMin + DATEDIFF(MI, #StartDate, DateAdd(hour, #EndHour, DateDiff(DAY, 0, #StartDate)))
Set #StartDate=DateAdd(hour, #StartHour+24, DateDiff(DAY, 0, #StartDate))
Set #Partial=0
End
Else Begin -- If this is a full day, add full minutes
Set #WorkMin=#WorkMin + (#EndHour-#StartHour)*60
Set #StartDate = DATEADD(DD,1,#StartDate)
End
End
Else Set (at)StartDate = DATEADD(HOUR, (at)StartHour, CAST(CAST(DATEADD(DD,1,(at)StartDate) AS DATE) AS DATETIME))
End
Else Set (at)StartDate = DATEADD(HOUR, (at)StartHour, CAST(CAST(DATEADD(DD,1,(at)StartDate) AS DATE) AS DATETIME))
End
If Datepart(dw,#StartDate)>1 And DATEPART(dw,#StartDate)<7 -- If last day is a weekday
If (Select Count(*) From #Holidays Where HDATE=DateAdd(dd, DateDiff(dd,0,#StartDate), 0)) = 0 -- And it is not a holiday
If #Partial=0 Set #WorkMin=#WorkMin + DATEDIFF(MI, #StartDate, #EndDate) Else Set #WorkMin=#WorkMin + DATEDIFF(MI, DateAdd(hour, #StartHour, DateDiff(DAY, 0, #StartDate)), #EndDate)
End
If #Reverse=1 Set #WorkMin=-#WorkMin
Return #WorkMin
End

Use TIME for start time. Then create INT variable to put converted value to minute to use for conditions.
I have created sample for you.
DECLARE #StartDate DATETIME='2018-12-10 10:00'
DECLARE #ExpectedStartTime TIME='08:30'
DECLARE #ExpectedStartMin INT=DATEPART(HOUR,#ExpectedStartTime)*60+DATEPART(MINUTE,#ExpectedStartTime)
DECLARE #ActualStartMin INT=DATEPART(HOUR,#StartDate)*60+DATEPART(MINUTE,#StartDate)
--Check before change the StartDate
SELECT #ExpectedStartMin, #ActualStartMin, #StartDate
If #ExpectedStartMin<#ActualStartMin Set #StartDate = CAST(CAST(#StartDate AS DATE) AS DATETIME)+#ExpectedStartTime
-- same way to the other conditions.
--
--
--Check after change the StartDate
SELECT #ExpectedStartMin, #ActualStartMin, #StartDate

Related

SQL-Server I'm getting crazy with workday calculation with datepart

I'm working on SQL-Server 2012 and have the following code example to get workdays between two dates
DECLARE #StartDate AS date
DECLARE #EndDate AS date
SET #StartDate = '2019/02/18' -- this is a monday
SET #EndDate = '2019/02/23' -- this is a saturday
SELECT
DATEDIFF(DD, #StartDate, #EndDate)
- (DATEDIFF(WK, #StartDate,#EndDate) * 2)
- CASE WHEN DATEPART(DW, #StartDate) = 1 THEN 1 ELSE 0 END
- CASE WHEN DATEPART(DW, #EndDate) = 1 THEN 1 ELSE 0 END
the result is 4, which is correct...
But If I put 2019/02/24 (sunday) for the EndDate I'm getting 3... ????
I'm getting crazy here...
You're validating your Enddate to be Sunday instead of Saturday. I had a similar function available that is independent of date settings.
SELECT ISNULL((((DATEDIFF(dd,#StartDate,#EndDate)) --Start with total number of days including weekends
- (DATEDIFF(wk,#StartDate,#EndDate)*2) --Subtact 2 days for each full weekend
- (1-SIGN(DATEDIFF(dd,6,#StartDate)%7)) --If StartDate is a Sunday, Subtract 1
- (1-SIGN(DATEDIFF(dd,5,#EndDate) %7)))) , 0) --If StartDate is a Saturday, Subtract 1
WHERE #StartDate <= #EndDate
The answer by #Luis Cazares works great except that when the start date or end date fall on a Saturday or Sunday, then the number is 1 off. Also, holidays are not accounted for (if that falls within the discussion).
Here's my way to construct a function.
CREATE FUNCTION [dbo].[getWorkDays] (#startDate date, #endDate date)
RETURNS int
AS
BEGIN
DECLARE #daysct int = 0;
DECLARE #currentDate date = #startDate;
IF #endDate>#startDate
GOTO COUNTIT;
ELSE
GOTO ZERO;
COUNTIT:
BEGIN
--Pre-process: if the startDate is holiday or weekend, then step in until the next business day
WHILE datepart(weekday, #currentDate) in (1, 7) OR EXISTS (SELECT holiday_date FROM dbo.holidays WHERE holiday_date = #currentDate) --if weekend or holiday
BEGIN
SET #currentDate = dateadd(day, 1, #currentDate);
SET #daysct=1;
END
WHILE #currentDate <= #endDate
BEGIN
IF #currentDate=#endDate
BREAK;
ELSE
IF datepart(weekday, #currentDate) not in (1, 7) AND NOT EXISTS (SELECT holiday_date FROM dbo.holidays WHERE holiday_date = #currentDate) --if not weekend and not holiday
BEGIN
SET #daysct=#daysct + 1;
END
SET #currentDate=dateadd(day, 1, #currentDate);
END
--Post-process: if the end date DOES NOT fall on a weekend or holiday, then #daysct is systematically 1 below. Add 1 to straighten it.
IF datepart(weekday, #endDate) in (1, 7) OR EXISTS (SELECT holiday_date FROM dbo.holidays WHERE holiday_date = #endDate)
SET #daysct = #daysct - 1;
RETURN(#daysct);
END
ZERO:
BEGIN
SET #daysct=0;
RETURN(#daysct);
END
END;
It's not the most efficient code but it's naturally the process how I would hand-count it. The catch is you need to declare a variable or constant, otherwise it will significantly slow down your queries.
DECLARE #days_worked int = dbo.getWorkDays(dateadd(day, 1, eomonth(getdate() AT TIME ZONE 'Pacific Standard Time', -1)),getdate() AT TIME ZONE 'Pacific Standard Time');
Also, before you can use it, make sure you have a table for the holidays. My table is constructed this way.
enter image description here
DECLARE #StartDate DATETIME
DECLARE #EndDate DATETIME
SET #StartDate = '2019/02/18'
SET #EndDate = '2019/02/23'
SELECT
(DATEDIFF(dd, #StartDate, #EndDate) + 1)
-(DATEDIFF(wk, #StartDate, #EndDate) * 2)
-(CASE WHEN DATENAME(dw, #StartDate) = 'Sunday' THEN 1 ELSE 0 END)
-(CASE WHEN DATENAME(dw, #EndDate) = 'Saturday' THEN 1 ELSE 0 END)

Replace while loop with set-based approach in SQL Server

I have this table which displays providers and total active clients on each day.
DailyClientPopulation table:
Provider Activeclients DateAdded
------- ------------- ---------
p1 10 2016-11-01
p1 15 2016-11-02
p2 14 2016-11-01
.
.
p1 70 2016-11-30
p2 50 2016-11-30
The result should be like this.means we need to display average number of client placed on first half and second half of every month.
TEMPBIWEEKLYCENSUS table:
Provider Avg(activeclients) Biweeklyrange
-------- ----------------- -------------
p1 30 11/01-11/15
p2 20 11/01-11/15
p1 40 11/15-11/30
p2 30 11/15-11/30
I am using while loop to display the results.and updating the start date and current end date values. for ex: for first half of november, start date=11/01 and current end date=11/15.
End date=end of the month.
This is the code:
DECLARE #STARTDATE DATETIME
DECLARE #ENDDATE DATETIME
DECLARE #CURRENTENDDATE DATETIME
DECLARE #MONTHLASTDATE DATETIME
DECLARE #DAYSTOADD INT
DECLARE #TEMPSTARTDATE DATETIME
SET #STARTDATE= CONVERT(DATE, DATEADD(DAY, -#NoOfCharts*15,GETDATE()))
--PRINT #STARTDATE
SET #STARTDATE = DATEADD(MONTH,DATEDIFF(MONTH, 0, #STARTDATE),0 )
--PRINT #STARTDATE
SET #ENDDATE = CONVERT(DATE,DATEADD(MONTH,1,GETDATE()))
SET #ENDDATE = DATEADD(DAY, -1, DATEADD(MONTH, DATEDIFF(MONTH, 0, #ENDDATE),0))
--PRINT #ENDDATE
DECLARE #TEMPBIWEEKLYCENSUS table (ProviderName NVARCHAR(500), ActiveClients INT, BiWeeklyRange NVARCHAR(50) );
-- SET #MONTHLASTDATE =DATEADD(DAY,-1, DATEADD(MONTH,1,#STARTDATE))
WHILE (#STARTDATE <= #ENDDATE)
BEGIN
SET #MONTHLASTDATE = DATEADD(DAY, -1, DATEADD(MONTH, 1, DATEADD(MONTH, DATEDIFF(MONTH, 0, #STARTDATE), 0)))
-- PRINT DATEDIFF(DAY,#STARTDATE, #MONTHLASTDATE)
IF DATEDIFF(DAY, #STARTDATE, #MONTHLASTDATE) > 15
BEGIN
IF DATEDIFF(DAY, #STARTDATE, #MONTHLASTDATE) / 2 = 15
BEGIN
SET #DAYSTOADD = 15
END
ELSE
BEGIN
SET #DAYSTOADD = 14
END
END
ELSE IF DATEDIFF(DAY, #STARTDATE, #MONTHLASTDATE) < 15
BEGIN
SET #DAYSTOADD = DATEDIFF(DAY,#STARTDATE, #MONTHLASTDATE)
END
SET #CURRENTENDDATE = CONVERT(DATE,DATEADD(DAY,#DAYSTOADD,#STARTDATE))
--PRINT '**************************************'
--PRINT 'STARTDATE'
--PRINT #STARTDATE
--PRINT 'CURRENTENDDATE'
--PRINT #CURRENTENDDATE
--PRINT '**************************************'
INSERT INTO #TEMPBIWEEKLYCENSUS
SELECT
[ProviderName],
AVG(ActiveClients),
CONVERT(VARCHAR(10), DATEPART(MONTH, #STARTDATE)) + '/' + CONVERT(VARCHAR(10), DATEPART(DAY, #STARTDATE)) + '-' + CONVERT(VARCHAR(10), DATEPART(MONTH, #CURRENTENDDATE)) + '/' + CONVERT(VARCHAR(10), DATEPART(DAY, #CURRENTENDDATE))
FROM
[dbo].[DailyClientPopulation]
WHERE
CONVERT(DATE, DateAdded) >= #STARTDATE
AND CONVERT(DATE, DateAdded) <= #CURRENTENDDATE
GROUP BY
ProviderName
SET #STARTDATE = CONVERT(DATE,DATEADD(DAY,1,#CURRENTENDDATE))
END
SELECT
ProviderName, ActiveClients, BiWeeklyRange
FROM
#TEMPBIWEEKLYCENSUS
Could you please suggest how to remove this while loop and convert the code to set-based approach.
You can cross apply some subqueries to your dbo.DailyClientPopulation using the DateAdded date values to produce BiWeekly begin and end dates, and then include between logic in the where clause. I have added a few months of sample data to a temp table to show how the date functions divide the months into BiWeekly ranges:
-- insert sample data
if object_id('tempdb..#DailyClientPopulation') is not null
drop table #DailyClientPopulation
go
create table #DailyClientPopulation
(
Provider char(2),
Activeclients int,
DateAdded datetime
)
insert into #DailyClientPopulation
values
('p1',10,'2016-11-01'),
('p1',15,'2016-11-02'),
('p2',14,'2016-11-01'),
('p1',70,'2016-11-30'),
('p2',50,'2016-11-30'),
('p1',10,'2016-12-01'),
('p1',15,'2016-12-02'),
('p2',14,'2016-12-01'),
('p1',70,'2016-12-30'),
('p2',50,'2016-12-30'),
('p1',10,'2017-01-01'),
('p1',15,'2017-01-02'),
('p2',14,'2017-01-01'),
('p1',70,'2017-01-30'),
('p2',50,'2017-01-30'),
('p1',10,'2017-02-01'),
('p1',15,'2017-02-02'),
('p2',14,'2017-02-01'),
('p1',70,'2017-02-28'),
('p2',50,'2017-02-28')
-- return AvgActiveClients per BiWeeklyRange
select
dcp.Provider,
avg(dcp.ActiveClients) as AvgActiveClients,
convert(varchar(10),bed.begin_date,101) + ' - ' + convert(varchar(10),bed.end_date,101) as BiWeeklyRange
from #DailyClientPopulation dcp
cross apply (values(dateadd(mm,datediff(mm,0,dcp.DateAdded),0))) bom(bom_date) -- begin of month
cross apply (values(dateadd(dd,-1,dateadd(mm,1,bom.bom_date)))) eom(eom_date) -- end of month
cross apply (values(dateadd(dd,day(eom.eom_date)/2,bom.bom_date))) bosh(bosh_date) -- begin of second half
cross apply (values(dateadd(dd,-1,bosh.bosh_date))) eofh(eofh_date) -- end of first half
cross apply (values(bom.bom_date,eofh.eofh_date),
(bosh.bosh_date,eom.eom_date)) bed(begin_date,end_date) -- begin / end dates
where dcp.DateAdded between bed.begin_date and bed.end_date
group by
dcp.Provider,
bed.begin_date,
bed.end_date
order by
bed.begin_date,
dcp.Provider

SQL Business Hours Calculation Error

Hoping someone can tell me where I am going wrong here, I started with a Business Hours query found at Calculate business hours between two dates, it's the second top answer because the top answer was not providing the correct output upon testing. The issue I am having is when I cross over to a new day and the end datetime exceeds the Business Day End. Here is my test:
select dbo.BusinessSeconds('09:00:00','17:00:00','2014-12-24 16:59:59','2014-12-26 18:00:00');
So if I go from 1 second before COB to the start of the next day, I get 1 second, which is correct. If I go from the same start to the end of the day the next day, I get 28801, or 1 full day and 1 second - which is also correct. However, if I extend the date end time to 17:00:01 through 17:59:59 it also includes those seconds but if I hit 18:00:00, the time truncates back to the original 17:00:00 end of day. So I will get correct data for any given end time unless it is during the 17:00:00-17:59:59 timeframes and then I potentially get up to an extra hour of time that I should not have.
Any help would be greatly appreciated as I have a deliverable on this by tomorrow.
ALTER Function [dbo].[BusinessSeconds](#BusinessDayStart TIME,#BusinessDayEnd TIME,#StartDate DATETIME,#EndDate DATETIME) Returns Int
AS
Begin
--TEST: select dbo.BusinessSeconds('09:00:00','17:00:00','2014-12-24 16:59:59','2014-12-26 18:00:00');
Declare #WorkMin INT = 0 -- Initialize counter
Declare #Reverse BIT -- Flag to hold if direction is reverse
Declare #StartHour TIME = #BusinessDayStart -- Start of business hours (can be supplied as an argument if needed)
Declare #EndHour TIME = #BusinessDayEnd -- End of business hours (can be supplied as an argument if needed)
Declare #Holidays Table (HDate DateTime) -- Table variable to hold holidayes
-- If dates are in reverse order, switch them and set flag
If #StartDate>#EndDate
Begin
Declare #TempDate DateTime=#StartDate
Set #StartDate=#EndDate
Set #EndDate=#TempDate
Set #Reverse=1
End
Else Set #Reverse = 0
-- 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 DATEADD(month,((LEFT(CAL_ID,4)-1900)*12)+LEFT(RIGHT(CAL_ID,4),2)-1,RIGHT(CAL_ID,2)-1) from B_CALENDAR where CAL_DAY_PK = 263 and HDATE >= DateAdd(dd, DateDiff(dd,0,#StartDate), 0)
Insert Into #Holidays (HDate) Select HolidayDate from V_BUS_HOL Where HolidayDate >= DateAdd(dd, DateDiff(dd,0,#StartDate), 0)
If CAST(#StartDate as TIME) < #StartHour Set #StartDate = CAST(CAST(#StartDate as DATE) as DATETIME) + CAST(#StartHour as DATETIME) -- If Start time is less than start hour, set it to start hour
If CAST(#StartDate as TIME) >= DATEADD(HOUR,1,#EndHour) Set #StartDate = CAST(CAST(DATEADD(DAY,1,#StartDate) as DATE) as DATETIME) + CAST(#StartHour as DATETIME) -- If Start time is after end hour, set it to start hour of next day
If CAST(#EndDate as TIME) >= DATEADD(HOUR,1,#EndHour) Set #EndDate = CAST(CAST(#EndDate as DATE) as DATETIME) + CAST(#EndHour as DATETIME) -- If End time is after end hour, set it to end hour
If CAST(#EndDate as TIME) < #StartHour Set #EndDate = CAST(CAST(DATEADD(DAY,-1,#EndDate) as DATE) as DATETIME) + CAST(#EndHour as DATETIME) -- If End time is before start hour, set it to end hour of previous day
If #StartDate>#EndDate Return 0
-- If Start and End is on same day
If DateDiff(Day,#StartDate,#EndDate) <= 0
Begin
If Datepart(dw,#StartDate)>1 And DATEPART(dw,#StartDate)<7 -- If day is between sunday and saturday
If (Select Count(*) From #Holidays Where HDATE=DateAdd(dd, DateDiff(dd,0,#StartDate), 0)) = 0 -- If day is not a holiday
If #EndDate<#StartDate Return 0 Else Set #WorkMin=DATEDIFF(SECOND, #StartDate, #EndDate) -- Calculate difference
Else Return 0
Else Return 0
End
Else Begin
Declare #Partial int=1 -- Set partial day flag
While DateDiff(Day,#StartDate,#EndDate) > 0 -- While start and end days are different
Begin
If Datepart(dw,#StartDate)>1 And DATEPART(dw,#StartDate)<7 -- If this is a weekday
Begin
If (Select Count(*) From #Holidays Where HDATE=DateAdd(dd, DateDiff(dd,0,#StartDate), 0)) = 0 -- If this is not a holiday
Begin
If #Partial=1 -- If this is the first iteration, calculate partial time
Begin
Set #WorkMin=#WorkMin + DATEDIFF(SECOND, #StartDate, DateAdd(hour, DATEPART(HOUR,#EndHour), DateDiff(DAY, 0, #StartDate)))
Set #StartDate=DateAdd(hour, DATEPART(HOUR,#StartHour)+24, DateDiff(DAY, 0, #StartDate))
Set #Partial=0
End
Else Begin -- If this is a full day, add full minutes
Set #WorkMin=#WorkMin + (DATEPART(HOUR,#EndHour)-DATEPART(HOUR,#StartHour))*60
Set #StartDate = DATEADD(DD,1,#StartDate)
End
End
Else Set #StartDate = DATEADD(DD,1,#StartDate)
End
Else Set #StartDate = DATEADD(DD,1,#StartDate)
End
If Datepart(dw,#StartDate)>1 And DATEPART(dw,#StartDate)<7 -- If last day is a weekday
If (Select Count(*) From #Holidays Where HDATE=DateAdd(dd, DateDiff(dd,0,#StartDate), 0)) = 0 -- And it is not a holiday
If #Partial=0 Set #WorkMin=#WorkMin + DATEDIFF(SECOND, #StartDate, #EndDate) Else Set #WorkMin=#WorkMin + DATEDIFF(SECOND, DateAdd(hour, DATEPART(HOUR,#StartHour), DateDiff(DAY, 0, #StartDate)), #EndDate)
End
If #Reverse=1 Set #WorkMin=-#WorkMin
Return #WorkMin
End
This one is an old one, however I believe that your problem is datetime/time data types. You didn't state the version of SQL Server you are using but if you have 2008 or above, you may want to adjust your tables and this function to use the datetime2 format. It is more accurate. datetime/smalldatetime both are only accurate to a certain point, then they round which could cause the problem you are looking at.

get DATEDIFF excluding weekends using sql server

I am using this query to get time taken.
SELECT DATEDIFF(dd, ActualStartDate, ActualCompletionDate) AS TimeTaken
FROM TableName
Now I want to exclude weekends and only include Mon-Fri as days counted.
Example query below, here are some details on how I solved it.
Using DATEDIFF(WK, ...) will give us the number of weeks between the 2 dates. SQL Server evaluates this as a difference between week numbers rather than based on the number of days. This is perfect, since we can use this to determine how many weekends passed between the dates.
So we can multiple that value by 2 to get the number of weekend days that occurred and subtract that from the DATEDIFF(dd, ...) to get the number of weekdays.
This doesn't behave 100% correctly when the start or end date falls on Sunday, though. So I added in some case logic at the end of the calculation to handle those instances.
You may also want to consider whether or not the DATEDIFF should be fully inclusive. e.g. Is the difference between 9/10 and 9/11 1 day or 2 days? If the latter, you'll want to add 1 to the final product.
declare #d1 datetime, #d2 datetime
select #d1 = '9/9/2011', #d2 = '9/18/2011'
select datediff(dd, #d1, #d2) - (datediff(wk, #d1, #d2) * 2) -
case when datepart(dw, #d1) = 1 then 1 else 0 end +
case when datepart(dw, #d2) = 1 then 1 else 0 end
I found when i used this there was a problem when d1 fell on saturday. Below is what i used to correct this.
declare #d1 datetime, #d2 datetime
select #d1 = '11/19/2011' , #d2 = '11/28/2011'
select datediff(dd, #d1, #d2) +case when datepart(dw, #d1) = 7 then 1 else 0 end - (datediff(wk, #d1, #d2) * 2) -
case when datepart(dw, #d1) = 1 then 1 else 0 end +
case when datepart(dw, #d2) = 1 then 1 else 0 end
BEGIN
DECLARE #totaldays INT;
DECLARE #weekenddays INT;
SET #totaldays = DATEDIFF(DAY, #startDate, #endDate)
SET #weekenddays = ((DATEDIFF(WEEK, #startDate, #endDate) * 2) + -- get the number of weekend days in between
CASE WHEN DATEPART(WEEKDAY, #startDate) = 1 THEN 1 ELSE 0 END + -- if selection was Sunday, won't add to weekends
CASE WHEN DATEPART(WEEKDAY, #endDate) = 6 THEN 1 ELSE 0 END) -- if selection was Saturday, won't add to weekends
Return (#totaldays - #weekenddays)
END
This is on SQL Server 2014
declare #d1 datetime, #d2 datetime
select #d1 = '4/19/2017', #d2 = '5/7/2017'
DECLARE #Counter int = datediff(DAY,#d1 ,#d2 )
DECLARE #C int = 0
DECLARE #SUM int = 0
WHILE #Counter > 0
begin
SET #SUM = #SUM + IIF(DATENAME(dw,
DATEADD(day,#c,#d1))IN('Sunday','Monday','Tuesday','Wednesday','Thursday')
,1,0)
SET #Counter = #Counter - 1
set #c = #c +1
end
select #Sum
If you hate CASE statements as much as I do, and want to be able to use the solution inline in your queries, just get the difference of days and subtract the count of weekend days and you'll have the desired result:
declare #d1 datetime, #d2 datetime, #days int
select #d1 = '2018/10/01', #d2 = '2018/11/01'
SET #days = DateDiff(dd, #d1, #d2) - DateDiff(ww, #d1, #d2)*2
print #days
(The only caveat--or at least point to keep in mind--is that this calculation is not inclusive of the last date, so you might need to add one day to the end date to achieve an inclusive result)
I just want to share the code I created that might help you.
DECLARE #MyCounter int = 0, #TempDate datetime, #EndDate datetime;
SET #TempDate = DATEADD(d,1,'2017-5-27')
SET #EndDate = '2017-6-3'
WHILE #TempDate <= #EndDate
BEGIN
IF DATENAME(DW,#TempDate) = 'Sunday' OR DATENAME(DW,#TempDate) = 'Saturday'
SET #MyCounter = #MyCounter
ELSE IF #TempDate not in ('2017-1-1', '2017-1-16', '2017-2-20', '2017-5-29', '2017-7-4', '2017-9-4', '2017-10-9', '2017-11-11', '2017-12-25')
SET #MyCounter = #MyCounter + 1
SET #TempDate = DATEADD(d,1,#TempDate)
CONTINUE
END
PRINT #MyCounter
PRINT #TempDate
If you do have a holiday table, you can also use that so that you don't have to list all the holidays in the ELSE IF section of the code. You can also create a function for this code and use the function whenever you need it in your query.
I hope this might help too.
Using https://stackoverflow.com/a/1804095 and JeffFisher30's answer above (https://stackoverflow.com/a/14572370/6147425) and my own need to have fractional days, I wrote this:
DateDiff(second,Start_Time,End_Time)/86400.0
-2*DateDiff(week, Start_Time, End_Time)
-Case When (DatePart(weekday, Start_Time)+##DateFirst)%7 = 1 Then 1 Else 0 End
+Case When (DatePart(weekday, End_Time)+##DateFirst)%7 = 1 Then 1 Else 0 End
Use this function to calculate the number of business days excluding Saturday and Sunday. Also it will exclude start date and it will include end date.
-- Select [dbo].[GetBussinessDays] ('02/18/2021', '03/06/2021') -- 11 days
CREATE or ALTER FUNCTION [dbo].[GetBussinessDays] (
#StartDate DATETIME,
#EndDate DATETIME
)
returns INT AS
BEGIN
DECLARE #tempStartDate DATETIME= #StartDate;
DECLARE #tempEndDate DATETIME = #EndDate;
IF(#tempStartDate IS NULL
OR
#tempEndDate IS NULL)
BEGIN
RETURN NULL;
END
--To avoid negative values reverse the date if StartDate is grater than EndDate
IF(#StartDate > #EndDate)
BEGIN
SET #StartDate = #tempEndDate;
SET #EndDate = #tempStartDate;
END
DECLARE #Counter INT = Datediff(day,#StartDate ,#EndDate);
DECLARE #TempCounter INT = 0;
DECLARE #TotalBusinessDays INT = 0;
WHILE #Counter >= 0
BEGIN
IF(#TempCounter > 0 OR #Counter = 1) -- To ignore first day's calculation
Begin
SET #TotalBusinessDays = #TotalBusinessDays + Iif(Datename(dw, Dateadd(day,#TempCounter,#StartDate)) IN('Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday'),1,0)
END
SET #Counter = #Counter - 1
SET #TempCounter = #TempCounter +1
END
RETURN #TotalBusinessDays;
END
Using #Derek Kromm answer (Current Marked Answer)
I have modified so it IS tolerant of any localisations that may be on the target server.
DECLARE #d1 DATETIME, #d2 DATETIME
SELECT #d1 = '10/01/2022', #d2 = '10/28/2022'
SELECT (datediff(dd, #d1, #EndQuery)+1) - (datediff(wk, #d1, dateadd(dd,1,#d2)) * 2)
- CASE WHEN DateName(WEEKDAY, #d1) = 'Sunday' THEN 1 ELSE 0 END -- This includes for start date edge case
+ CASE WHEN DateName(WEEKDAY, #d2) = 'Saturday' THEN 1 ELSE 0 END -- This includes for end date edge case.
This is with the end date being innclusive.
/*
EXAMPLE:
/MONDAY/
SET DATEFIRST 1
SELECT dbo.FUNC_GETDATEDIFFERENCE_WO_WEEKEND('2019-02-01','2019-02-12')
*/
CREATE FUNCTION FUNC_GETDATEDIFFERENCE_WO_WEEKEND
(
#pdtmaLastLoanPayDate DATETIME,
#pdtmaDisbursedDate DATETIME
)
RETURNS BIGINT
BEGIN
DECLARE
#mintDaysDifference BIGINT
SET #mintDaysDifference = 0
WHILE CONVERT(NCHAR(10),#pdtmaLastLoanPayDate,121) <= CONVERT(NCHAR(10),#pdtmaDisbursedDate,121)
BEGIN
IF DATEPART(WEEKDAY,#pdtmaLastLoanPayDate) NOT IN (6,7)
BEGIN
SET #mintDaysDifference = #mintDaysDifference + 1
END
SET #pdtmaLastLoanPayDate = DATEADD(DAY,1,#pdtmaLastLoanPayDate)
END
RETURN ISNULL(#mintDaysDifference,0)
END

Count work days between two dates

How can I calculate the number of work days between two dates in SQL Server?
Monday to Friday and it must be T-SQL.
For workdays, Monday to Friday, you can do it with a single SELECT, like this:
DECLARE #StartDate DATETIME
DECLARE #EndDate DATETIME
SET #StartDate = '2008/10/01'
SET #EndDate = '2008/10/31'
SELECT
(DATEDIFF(dd, #StartDate, #EndDate) + 1)
-(DATEDIFF(wk, #StartDate, #EndDate) * 2)
-(CASE WHEN DATENAME(dw, #StartDate) = 'Sunday' THEN 1 ELSE 0 END)
-(CASE WHEN DATENAME(dw, #EndDate) = 'Saturday' THEN 1 ELSE 0 END)
If you want to include holidays, you have to work it out a bit...
In Calculating Work Days you can find a good article about this subject, but as you can see it is not that advanced.
--Changing current database to the Master database allows function to be shared by everyone.
USE MASTER
GO
--If the function already exists, drop it.
IF EXISTS
(
SELECT *
FROM dbo.SYSOBJECTS
WHERE ID = OBJECT_ID(N'[dbo].[fn_WorkDays]')
AND XType IN (N'FN', N'IF', N'TF')
)
DROP FUNCTION [dbo].[fn_WorkDays]
GO
CREATE FUNCTION dbo.fn_WorkDays
--Presets
--Define the input parameters (OK if reversed by mistake).
(
#StartDate DATETIME,
#EndDate DATETIME = NULL --#EndDate replaced by #StartDate when DEFAULTed
)
--Define the output data type.
RETURNS INT
AS
--Calculate the RETURN of the function.
BEGIN
--Declare local variables
--Temporarily holds #EndDate during date reversal.
DECLARE #Swap DATETIME
--If the Start Date is null, return a NULL and exit.
IF #StartDate IS NULL
RETURN NULL
--If the End Date is null, populate with Start Date value so will have two dates (required by DATEDIFF below).
IF #EndDate IS NULL
SELECT #EndDate = #StartDate
--Strip the time element from both dates (just to be safe) by converting to whole days and back to a date.
--Usually faster than CONVERT.
--0 is a date (01/01/1900 00:00:00.000)
SELECT #StartDate = DATEADD(dd,DATEDIFF(dd,0,#StartDate), 0),
#EndDate = DATEADD(dd,DATEDIFF(dd,0,#EndDate) , 0)
--If the inputs are in the wrong order, reverse them.
IF #StartDate > #EndDate
SELECT #Swap = #EndDate,
#EndDate = #StartDate,
#StartDate = #Swap
--Calculate and return the number of workdays using the input parameters.
--This is the meat of the function.
--This is really just one formula with a couple of parts that are listed on separate lines for documentation purposes.
RETURN (
SELECT
--Start with total number of days including weekends
(DATEDIFF(dd,#StartDate, #EndDate)+1)
--Subtact 2 days for each full weekend
-(DATEDIFF(wk,#StartDate, #EndDate)*2)
--If StartDate is a Sunday, Subtract 1
-(CASE WHEN DATENAME(dw, #StartDate) = 'Sunday'
THEN 1
ELSE 0
END)
--If EndDate is a Saturday, Subtract 1
-(CASE WHEN DATENAME(dw, #EndDate) = 'Saturday'
THEN 1
ELSE 0
END)
)
END
GO
If you need to use a custom calendar, you might need to add some checks and some parameters. Hopefully it will provide a good starting point.
All Credit to Bogdan Maxim & Peter Mortensen. This is their post, I just added holidays to the function (This assumes you have a table "tblHolidays" with a datetime field "HolDate".
--Changing current database to the Master database allows function to be shared by everyone.
USE MASTER
GO
--If the function already exists, drop it.
IF EXISTS
(
SELECT *
FROM dbo.SYSOBJECTS
WHERE ID = OBJECT_ID(N'[dbo].[fn_WorkDays]')
AND XType IN (N'FN', N'IF', N'TF')
)
DROP FUNCTION [dbo].[fn_WorkDays]
GO
CREATE FUNCTION dbo.fn_WorkDays
--Presets
--Define the input parameters (OK if reversed by mistake).
(
#StartDate DATETIME,
#EndDate DATETIME = NULL --#EndDate replaced by #StartDate when DEFAULTed
)
--Define the output data type.
RETURNS INT
AS
--Calculate the RETURN of the function.
BEGIN
--Declare local variables
--Temporarily holds #EndDate during date reversal.
DECLARE #Swap DATETIME
--If the Start Date is null, return a NULL and exit.
IF #StartDate IS NULL
RETURN NULL
--If the End Date is null, populate with Start Date value so will have two dates (required by DATEDIFF below).
IF #EndDate IS NULL
SELECT #EndDate = #StartDate
--Strip the time element from both dates (just to be safe) by converting to whole days and back to a date.
--Usually faster than CONVERT.
--0 is a date (01/01/1900 00:00:00.000)
SELECT #StartDate = DATEADD(dd,DATEDIFF(dd,0,#StartDate), 0),
#EndDate = DATEADD(dd,DATEDIFF(dd,0,#EndDate) , 0)
--If the inputs are in the wrong order, reverse them.
IF #StartDate > #EndDate
SELECT #Swap = #EndDate,
#EndDate = #StartDate,
#StartDate = #Swap
--Calculate and return the number of workdays using the input parameters.
--This is the meat of the function.
--This is really just one formula with a couple of parts that are listed on separate lines for documentation purposes.
RETURN (
SELECT
--Start with total number of days including weekends
(DATEDIFF(dd,#StartDate, #EndDate)+1)
--Subtact 2 days for each full weekend
-(DATEDIFF(wk,#StartDate, #EndDate)*2)
--If StartDate is a Sunday, Subtract 1
-(CASE WHEN DATENAME(dw, #StartDate) = 'Sunday'
THEN 1
ELSE 0
END)
--If EndDate is a Saturday, Subtract 1
-(CASE WHEN DATENAME(dw, #EndDate) = 'Saturday'
THEN 1
ELSE 0
END)
--Subtract all holidays
-(Select Count(*) from [DB04\DB04].[Gateway].[dbo].[tblHolidays]
where [HolDate] between #StartDate and #EndDate )
)
END
GO
-- Test Script
/*
declare #EndDate datetime= dateadd(m,2,getdate())
print #EndDate
select [Master].[dbo].[fn_WorkDays] (getdate(), #EndDate)
*/
My version of the accepted answer as a function using DATEPART, so I don't have to do a string comparison on the line with
DATENAME(dw, #StartDate) = 'Sunday'
Anyway, here's my business datediff function
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE FUNCTION BDATEDIFF
(
#startdate as DATETIME,
#enddate as DATETIME
)
RETURNS INT
AS
BEGIN
DECLARE #res int
SET #res = (DATEDIFF(dd, #startdate, #enddate) + 1)
-(DATEDIFF(wk, #startdate, #enddate) * 2)
-(CASE WHEN DATEPART(dw, #startdate) = 1 THEN 1 ELSE 0 END)
-(CASE WHEN DATEPART(dw, #enddate) = 7 THEN 1 ELSE 0 END)
RETURN #res
END
GO
Another approach to calculating working days is to use a WHILE loop which basically iterates through a date range and increment it by 1 whenever days are found to be within Monday – Friday. The complete script for calculating working days using the WHILE loop is shown below:
CREATE FUNCTION [dbo].[fn_GetTotalWorkingDaysUsingLoop]
(#DateFrom DATE,
#DateTo   DATE
)
RETURNS INT
AS
     BEGIN
         DECLARE #TotWorkingDays INT= 0;
         WHILE #DateFrom <= #DateTo
             BEGIN
                 IF DATENAME(WEEKDAY, #DateFrom) IN('Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday')
                     BEGIN
                         SET #TotWorkingDays = #TotWorkingDays + 1;
                 END;
                 SET #DateFrom = DATEADD(DAY, 1, #DateFrom);
             END;
         RETURN #TotWorkingDays;
     END;
GO
Although the WHILE loop option is cleaner and uses less lines of code, it has the potential of being a performance bottleneck in your environment particularly when your date range spans across several years.
You can see more methods on how to calculate work days and hours in this article:
https://www.sqlshack.com/how-to-calculate-work-days-and-hours-in-sql-server/
DECLARE #TotalDays INT,#WorkDays INT
DECLARE #ReducedDayswithEndDate INT
DECLARE #WeekPart INT
DECLARE #DatePart INT
SET #TotalDays= DATEDIFF(day, #StartDate, #EndDate) +1
SELECT #ReducedDayswithEndDate = CASE DATENAME(weekday, #EndDate)
WHEN 'Saturday' THEN 1
WHEN 'Sunday' THEN 2
ELSE 0 END
SET #TotalDays=#TotalDays-#ReducedDayswithEndDate
SET #WeekPart=#TotalDays/7;
SET #DatePart=#TotalDays%7;
SET #WorkDays=(#WeekPart*5)+#DatePart
RETURN #WorkDays
(I'm a few points shy of commenting privileges)
If you decide to forgo the +1 day in CMS's elegant solution, note that if your start date and end date are in the same weekend, you get a negative answer. Ie., 2008/10/26 to 2008/10/26 returns -1.
my rather simplistic solution:
select #Result = (..CMS's answer..)
if (#Result < 0)
select #Result = 0
RETURN #Result
.. which also sets all erroneous posts with start date after end date to zero. Something you may or may not be looking for.
For difference between dates including holidays I went this way:
1) Table with Holidays:
CREATE TABLE [dbo].[Holiday](
[Id] [int] IDENTITY(1,1) NOT NULL,
[Name] [nvarchar](50) NULL,
[Date] [datetime] NOT NULL)
2) I had my plannings Table like this and wanted to fill column Work_Days which was empty:
CREATE TABLE [dbo].[Plan_Phase](
[Id] [int] IDENTITY(1,1) NOT NULL,
[Id_Plan] [int] NOT NULL,
[Id_Phase] [int] NOT NULL,
[Start_Date] [datetime] NULL,
[End_Date] [datetime] NULL,
[Work_Days] [int] NULL)
3) So in order to get "Work_Days" to later fill in my column just had to:
SELECT Start_Date, End_Date,
(DATEDIFF(dd, Start_Date, End_Date) + 1)
-(DATEDIFF(wk, Start_Date, End_Date) * 2)
-(SELECT COUNT(*) From Holiday Where Date >= Start_Date AND Date <= End_Date)
-(CASE WHEN DATENAME(dw, Start_Date) = 'Sunday' THEN 1 ELSE 0 END)
-(CASE WHEN DATENAME(dw, End_Date) = 'Saturday' THEN 1 ELSE 0 END)
-(CASE WHEN (SELECT COUNT(*) From Holiday Where Start_Date = Date) > 0 THEN 1 ELSE 0 END)
-(CASE WHEN (SELECT COUNT(*) From Holiday Where End_Date = Date) > 0 THEN 1 ELSE 0 END) AS Work_Days
from Plan_Phase
Hope that I could help.
Cheers
Here is a version that works well (I think). Holiday table contains Holiday_date columns that contains holidays your company observe.
DECLARE #RAWDAYS INT
SELECT #RAWDAYS = DATEDIFF(day, #StartDate, #EndDate )--+1
-( 2 * DATEDIFF( week, #StartDate, #EndDate ) )
+ CASE WHEN DATENAME(dw, #StartDate) = 'Saturday' THEN 1 ELSE 0 END
- CASE WHEN DATENAME(dw, #EndDate) = 'Saturday' THEN 1 ELSE 0 END
SELECT #RAWDAYS - COUNT(*)
FROM HOLIDAY NumberOfBusinessDays
WHERE [Holiday_Date] BETWEEN #StartDate+1 AND #EndDate
I know this is an old question but I needed a formula for workdays excluding the start date since I have several items and need the days to accumulate correctly.
None of the non-iterative answers worked for me.
I used a defintion like
Number of times midnight to monday, tuesday, wednesday, thursday and friday is passed
(others might count midnight to saturday instead of monday)
I ended up with this formula
SELECT DATEDIFF(day, #StartDate, #EndDate) /* all midnights passed */
- DATEDIFF(week, #StartDate, #EndDate) /* remove sunday midnights */
- DATEDIFF(week, DATEADD(day, 1, #StartDate), DATEADD(day, 1, #EndDate)) /* remove saturday midnights */
This is basically CMS's answer without the reliance on a particular language setting. And since we're shooting for generic, that means it should work for all ##datefirst settings as well.
datediff(day, <start>, <end>) + 1 - datediff(week, <start>, <end>) * 2
/* if start is a Sunday, adjust by -1 */
+ case when datepart(weekday, <start>) = 8 - ##datefirst then -1 else 0 end
/* if end is a Saturday, adjust by -1 */
+ case when datepart(weekday, <end>) = (13 - ##datefirst) % 7 + 1 then -1 else 0 end
datediff(week, ...) always uses a Saturday-to-Sunday boundary for weeks, so that expression is deterministic and doesn't need to be modified (as long as our definition of weekdays is consistently Monday through Friday.) Day numbering does vary according to the ##datefirst setting and the modified calculations handle this correction with the small complication of some modular arithmetic.
A cleaner way to deal with the Saturday/Sunday thing is to translate the dates prior to extracting a day of week value. After shifting, the values will be back in line with a fixed (and probably more familiar) numbering that starts with 1 on Sunday and ends with 7 on Saturday.
datediff(day, <start>, <end>) + 1 - datediff(week, <start>, <end>) * 2
+ case when datepart(weekday, dateadd(day, ##datefirst, <start>)) = 1 then -1 else 0 end
+ case when datepart(weekday, dateadd(day, ##datefirst, <end>)) = 7 then -1 else 0 end
I've tracked this form of the solution back at least as far as 2002 and an Itzik Ben-Gan article. (https://technet.microsoft.com/en-us/library/aa175781(v=sql.80).aspx) Though it needed a small tweak since newer date types don't allow date arithmetic, it is otherwise identical.
EDIT:
I added back the +1 that had somehow been left off. It's also worth noting that this method always counts the start and end days. It also assumes that the end date is on or after the start date.
None of the functions above work for the same week or deal with holidays. I wrote this:
create FUNCTION [dbo].[ShiftHolidayToWorkday](#date date)
RETURNS date
AS
BEGIN
IF DATENAME( dw, #Date ) = 'Saturday'
SET #Date = DATEADD(day, - 1, #Date)
ELSE IF DATENAME( dw, #Date ) = 'Sunday'
SET #Date = DATEADD(day, 1, #Date)
RETURN #date
END
GO
create FUNCTION [dbo].[GetHoliday](#date date)
RETURNS varchar(50)
AS
BEGIN
declare #s varchar(50)
SELECT #s = CASE
WHEN dbo.ShiftHolidayToWorkday(CONVERT(varchar, [Year] ) + '-01-01') = #date THEN 'New Year'
WHEN dbo.ShiftHolidayToWorkday(CONVERT(varchar, [Year]+1) + '-01-01') = #date THEN 'New Year'
WHEN dbo.ShiftHolidayToWorkday(CONVERT(varchar, [Year] ) + '-07-04') = #date THEN 'Independence Day'
WHEN dbo.ShiftHolidayToWorkday(CONVERT(varchar, [Year] ) + '-12-25') = #date THEN 'Christmas Day'
--WHEN dbo.ShiftHolidayToWorkday(CONVERT(varchar, [Year]) + '-12-31') = #date THEN 'New Years Eve'
--WHEN dbo.ShiftHolidayToWorkday(CONVERT(varchar, [Year]) + '-11-11') = #date THEN 'Veteran''s Day'
WHEN [Month] = 1 AND [DayOfMonth] BETWEEN 15 AND 21 AND [DayName] = 'Monday' THEN 'Martin Luther King Day'
WHEN [Month] = 5 AND [DayOfMonth] >= 25 AND [DayName] = 'Monday' THEN 'Memorial Day'
WHEN [Month] = 9 AND [DayOfMonth] <= 7 AND [DayName] = 'Monday' THEN 'Labor Day'
WHEN [Month] = 11 AND [DayOfMonth] BETWEEN 22 AND 28 AND [DayName] = 'Thursday' THEN 'Thanksgiving Day'
WHEN [Month] = 11 AND [DayOfMonth] BETWEEN 23 AND 29 AND [DayName] = 'Friday' THEN 'Day After Thanksgiving'
ELSE NULL END
FROM (
SELECT
[Year] = YEAR(#date),
[Month] = MONTH(#date),
[DayOfMonth] = DAY(#date),
[DayName] = DATENAME(weekday,#date)
) c
RETURN #s
END
GO
create FUNCTION [dbo].GetHolidays(#year int)
RETURNS TABLE
AS
RETURN (
select dt, dbo.GetHoliday(dt) as Holiday
from (
select dateadd(day, number, convert(varchar,#year) + '-01-01') dt
from master..spt_values
where type='p'
) d
where year(dt) = #year and dbo.GetHoliday(dt) is not null
)
create proc UpdateHolidaysTable
as
if not exists(select TABLE_NAME from INFORMATION_SCHEMA.TABLES where TABLE_NAME = 'Holidays')
create table Holidays(dt date primary key clustered, Holiday varchar(50))
declare #year int
set #year = 1990
while #year < year(GetDate()) + 20
begin
insert into Holidays(dt, Holiday)
select a.dt, a.Holiday
from dbo.GetHolidays(#year) a
left join Holidays b on b.dt = a.dt
where b.dt is null
set #year = #year + 1
end
create FUNCTION [dbo].[GetWorkDays](#StartDate DATE = NULL, #EndDate DATE = NULL)
RETURNS INT
AS
BEGIN
IF #StartDate IS NULL OR #EndDate IS NULL
RETURN 0
IF #StartDate >= #EndDate
RETURN 0
DECLARE #Days int
SET #Days = 0
IF year(#StartDate) * 100 + datepart(week, #StartDate) = year(#EndDate) * 100 + datepart(week, #EndDate)
--same week
select #Days = (DATEDIFF(dd, #StartDate, #EndDate))
- (CASE WHEN DATENAME(dw, #StartDate) = 'Sunday' THEN 1 ELSE 0 END)
- (CASE WHEN DATENAME(dw, #EndDate) = 'Saturday' THEN 1 ELSE 0 END)
- (select count(*) from Holidays where dt between #StartDate and #EndDate)
ELSE
--diff weeks
select #Days = (DATEDIFF(dd, #StartDate, #EndDate) + 1)
- (DATEDIFF(wk, #StartDate, #EndDate) * 2)
- (CASE WHEN DATENAME(dw, #StartDate) = 'Sunday' THEN 1 ELSE 0 END)
- (CASE WHEN DATENAME(dw, #EndDate) = 'Saturday' THEN 1 ELSE 0 END)
- (select count(*) from Holidays where dt between #StartDate and #EndDate)
RETURN #Days
END
Using a date table:
DECLARE
#StartDate date = '2014-01-01',
#EndDate date = '2014-01-31';
SELECT
COUNT(*) As NumberOfWeekDays
FROM dbo.Calendar
WHERE CalendarDate BETWEEN #StartDate AND #EndDate
AND IsWorkDay = 1;
If you don't have that, you can use a numbers table:
DECLARE
#StartDate datetime = '2014-01-01',
#EndDate datetime = '2014-01-31';
SELECT
SUM(CASE WHEN DATEPART(dw, DATEADD(dd, Number-1, #StartDate)) BETWEEN 2 AND 6 THEN 1 ELSE 0 END) As NumberOfWeekDays
FROM dbo.Numbers
WHERE Number <= DATEDIFF(dd, #StartDate, #EndDate) + 1 -- Number table starts at 1, we want a 0 base
They should both be fast and it takes out the ambiguity/complexity. The first option is the best but if you don't have a calendar table you can allways create a numbers table with a CTE.
DECLARE #StartDate datetime,#EndDate datetime
select #StartDate='3/2/2010', #EndDate='3/7/2010'
DECLARE #TotalDays INT,#WorkDays INT
DECLARE #ReducedDayswithEndDate INT
DECLARE #WeekPart INT
DECLARE #DatePart INT
SET #TotalDays= DATEDIFF(day, #StartDate, #EndDate) +1
SELECT #ReducedDayswithEndDate = CASE DATENAME(weekday, #EndDate)
WHEN 'Saturday' THEN 1
WHEN 'Sunday' THEN 2
ELSE 0 END
SET #TotalDays=#TotalDays-#ReducedDayswithEndDate
SET #WeekPart=#TotalDays/7;
SET #DatePart=#TotalDays%7;
SET #WorkDays=(#WeekPart*5)+#DatePart
SELECT #WorkDays
CREATE FUNCTION x
(
#StartDate DATETIME,
#EndDate DATETIME
)
RETURNS INT
AS
BEGIN
DECLARE #Teller INT
SET #StartDate = DATEADD(dd,1,#StartDate)
SET #Teller = 0
IF DATEDIFF(dd,#StartDate,#EndDate) <= 0
BEGIN
SET #Teller = 0
END
ELSE
BEGIN
WHILE
DATEDIFF(dd,#StartDate,#EndDate) >= 0
BEGIN
IF DATEPART(dw,#StartDate) < 6
BEGIN
SET #Teller = #Teller + 1
END
SET #StartDate = DATEADD(dd,1,#StartDate)
END
END
RETURN #Teller
END
I took the various examples here, but in my particular situation we have a #PromisedDate for delivery and a #ReceivedDate for the actual receipt of the item. When an item was received before the "PromisedDate" the calculations were not totaling correctly unless I ordered the dates passed into the function by calendar order. Not wanting to check the dates every time, I changed the function to handle this for me.
Create FUNCTION [dbo].[fnGetBusinessDays]
(
#PromiseDate date,
#ReceivedDate date
)
RETURNS integer
AS
BEGIN
DECLARE #days integer
SELECT #days =
Case when #PromiseDate > #ReceivedDate Then
DATEDIFF(d,#PromiseDate,#ReceivedDate) +
ABS(DATEDIFF(wk,#PromiseDate,#ReceivedDate)) * 2 +
CASE
WHEN DATENAME(dw, #PromiseDate) <> 'Saturday' AND DATENAME(dw, #ReceivedDate) = 'Saturday' THEN 1
WHEN DATENAME(dw, #PromiseDate) = 'Saturday' AND DATENAME(dw, #ReceivedDate) <> 'Saturday' THEN -1
ELSE 0
END +
(Select COUNT(*) FROM CompanyHolidays
WHERE HolidayDate BETWEEN #ReceivedDate AND #PromiseDate
AND DATENAME(dw, HolidayDate) <> 'Saturday' AND DATENAME(dw, HolidayDate) <> 'Sunday')
Else
DATEDIFF(d,#PromiseDate,#ReceivedDate) -
ABS(DATEDIFF(wk,#PromiseDate,#ReceivedDate)) * 2 -
CASE
WHEN DATENAME(dw, #PromiseDate) <> 'Saturday' AND DATENAME(dw, #ReceivedDate) = 'Saturday' THEN 1
WHEN DATENAME(dw, #PromiseDate) = 'Saturday' AND DATENAME(dw, #ReceivedDate) <> 'Saturday' THEN -1
ELSE 0
END -
(Select COUNT(*) FROM CompanyHolidays
WHERE HolidayDate BETWEEN #PromiseDate and #ReceivedDate
AND DATENAME(dw, HolidayDate) <> 'Saturday' AND DATENAME(dw, HolidayDate) <> 'Sunday')
End
RETURN (#days)
END
If you need to add work days to a given date, you can create a function that depends on a calendar table, described below:
CREATE TABLE Calendar
(
dt SMALLDATETIME PRIMARY KEY,
IsWorkDay BIT
);
--fill the rows with normal days, weekends and holidays.
create function AddWorkingDays (#initialDate smalldatetime, #numberOfDays int)
returns smalldatetime as
begin
declare #result smalldatetime
set #result =
(
select t.dt from
(
select dt, ROW_NUMBER() over (order by dt) as daysAhead from calendar
where dt > #initialDate
and IsWorkDay = 1
) t
where t.daysAhead = #numberOfDays
)
return #result
end
As with DATEDIFF, I do not consider the end date to be part of the interval.
The number of (for example) Sundays between #StartDate and #EndDate is the number of Sundays between an "initial" Monday and the #EndDate minus the number of Sundays between this "initial" Monday and the #StartDate. Knowing this, we can calculate the number of workdays as follows:
DECLARE #StartDate DATETIME
DECLARE #EndDate DATETIME
SET #StartDate = '2018/01/01'
SET #EndDate = '2019/01/01'
SELECT DATEDIFF(Day, #StartDate, #EndDate) -- Total Days
- (DATEDIFF(Day, 0, #EndDate)/7 - DATEDIFF(Day, 0, #StartDate)/7) -- Sundays
- (DATEDIFF(Day, -1, #EndDate)/7 - DATEDIFF(Day, -1, #StartDate)/7) -- Saturdays
Best regards!
I borrowed some ideas from others to create my solution. I use inline code to ignore weekends and U.S. federal holidays. In my environment, EndDate may be null, but it will never precede StartDate.
CREATE FUNCTION dbo.ufn_CalculateBusinessDays(
#StartDate DATE,
#EndDate DATE = NULL)
RETURNS INT
AS
BEGIN
DECLARE #TotalBusinessDays INT = 0;
DECLARE #TestDate DATE = #StartDate;
IF #EndDate IS NULL
RETURN NULL;
WHILE #TestDate < #EndDate
BEGIN
DECLARE #Month INT = DATEPART(MM, #TestDate);
DECLARE #Day INT = DATEPART(DD, #TestDate);
DECLARE #DayOfWeek INT = DATEPART(WEEKDAY, #TestDate) - 1; --Monday = 1, Tuesday = 2, etc.
DECLARE #DayOccurrence INT = (#Day - 1) / 7 + 1; --Nth day of month (3rd Monday, for example)
--Increment business day counter if not a weekend or holiday
SELECT #TotalBusinessDays += (
SELECT CASE
--Saturday OR Sunday
WHEN #DayOfWeek IN (6,7) THEN 0
--New Year's Day
WHEN #Month = 1 AND #Day = 1 THEN 0
--MLK Jr. Day
WHEN #Month = 1 AND #DayOfWeek = 1 AND #DayOccurrence = 3 THEN 0
--G. Washington's Birthday
WHEN #Month = 2 AND #DayOfWeek = 1 AND #DayOccurrence = 3 THEN 0
--Memorial Day
WHEN #Month = 5 AND #DayOfWeek = 1 AND #Day BETWEEN 25 AND 31 THEN 0
--Independence Day
WHEN #Month = 7 AND #Day = 4 THEN 0
--Labor Day
WHEN #Month = 9 AND #DayOfWeek = 1 AND #DayOccurrence = 1 THEN 0
--Columbus Day
WHEN #Month = 10 AND #DayOfWeek = 1 AND #DayOccurrence = 2 THEN 0
--Veterans Day
WHEN #Month = 11 AND #Day = 11 THEN 0
--Thanksgiving
WHEN #Month = 11 AND #DayOfWeek = 4 AND #DayOccurrence = 4 THEN 0
--Christmas
WHEN #Month = 12 AND #Day = 25 THEN 0
ELSE 1
END AS Result);
SET #TestDate = DATEADD(dd, 1, #TestDate);
END
RETURN #TotalBusinessDays;
END
That's working for me, in my country on Saturday and Sunday are non-working days.
For me is important the time of #StartDate and #EndDate.
CREATE FUNCTION [dbo].[fnGetCountWorkingBusinessDays]
(
#StartDate as DATETIME,
#EndDate as DATETIME
)
RETURNS INT
AS
BEGIN
DECLARE #res int
SET #StartDate = CASE
WHEN DATENAME(dw, #StartDate) = 'Saturday' THEN DATEADD(dd, 2, DATEDIFF(dd, 0, #StartDate))
WHEN DATENAME(dw, #StartDate) = 'Sunday' THEN DATEADD(dd, 1, DATEDIFF(dd, 0, #StartDate))
ELSE #StartDate END
SET #EndDate = CASE
WHEN DATENAME(dw, #EndDate) = 'Saturday' THEN DATEADD(dd, 0, DATEDIFF(dd, 0, #EndDate))
WHEN DATENAME(dw, #EndDate) = 'Sunday' THEN DATEADD(dd, -1, DATEDIFF(dd, 0, #EndDate))
ELSE #EndDate END
SET #res =
(DATEDIFF(hour, #StartDate, #EndDate) / 24)
- (DATEDIFF(wk, #StartDate, #EndDate) * 2)
SET #res = CASE WHEN #res < 0 THEN 0 ELSE #res END
RETURN #res
END
GO
Create function like:
CREATE FUNCTION dbo.fn_WorkDays(#StartDate DATETIME, #EndDate DATETIME= NULL )
RETURNS INT
AS
BEGIN
DECLARE #Days int
SET #Days = 0
IF #EndDate = NULL
SET #EndDate = EOMONTH(#StartDate) --last date of the month
WHILE DATEDIFF(dd,#StartDate,#EndDate) >= 0
BEGIN
IF DATENAME(dw, #StartDate) <> 'Saturday'
and DATENAME(dw, #StartDate) <> 'Sunday'
and Not ((Day(#StartDate) = 1 And Month(#StartDate) = 1)) --New Year's Day.
and Not ((Day(#StartDate) = 4 And Month(#StartDate) = 7)) --Independence Day.
BEGIN
SET #Days = #Days + 1
END
SET #StartDate = DATEADD(dd,1,#StartDate)
END
RETURN #Days
END
You can call the function like:
select dbo.fn_WorkDays('1/1/2016', '9/25/2016')
Or like:
select dbo.fn_WorkDays(StartDate, EndDate)
from table1
Create Function dbo.DateDiff_WeekDays
(
#StartDate DateTime,
#EndDate DateTime
)
Returns Int
As
Begin
Declare #Result Int = 0
While #StartDate <= #EndDate
Begin
If DateName(DW, #StartDate) not in ('Saturday','Sunday')
Begin
Set #Result = #Result +1
End
Set #StartDate = DateAdd(Day, +1, #StartDate)
End
Return #Result
End
I found the below TSQL a fairly elegant solution (I don't have permissions to run functions). I found the DATEDIFF ignores DATEFIRST and I wanted my first day of the week to be a Monday. I also wanted the first working day to be set a zero and if it falls on a weekend Monday will be a zero. This may help someone who has a slightly different requirement :)
It does not handle bank holidays
SET DATEFIRST 1
SELECT
,(DATEDIFF(DD, [StartDate], [EndDate]))
-(DATEDIFF(wk, [StartDate], [EndDate]))
-(DATEDIFF(wk, DATEADD(dd,-##DATEFIRST,[StartDate]), DATEADD(dd,-##DATEFIRST,[EndDate]))) AS [WorkingDays]
FROM /*Your Table*/
One approach is to 'walk the dates' from start to finish in conjunction with a case expression which checks if the day is not a Saturday or a Sunday and flagging it(1 for weekday, 0 for weekend). And in the end just sum flags(it would be equal to the count of 1-flags as the other flag is 0) to give you the number of weekdays.
You can use a GetNums(startNumber,endNumber) type of utility function which generates a series of numbers for 'looping' from start date to end date. Refer http://tsql.solidq.com/SourceCodes/GetNums.txt for an implementation. The logic can also be extended to cater for holidays(say if you have a holidays table)
declare #date1 as datetime = '19900101'
declare #date2 as datetime = '19900120'
select sum(case when DATENAME(DW,currentDate) not in ('Saturday', 'Sunday') then 1 else 0 end) as noOfWorkDays
from dbo.GetNums(0,DATEDIFF(day,#date1, #date2)-1) as Num
cross apply (select DATEADD(day,n,#date1)) as Dates(currentDate)